A Beginner's Guide to Unit Testing with Hypothesis
Hypothesis is a Python testing library that helps you find bugs and edge cases that regular unit tests often miss.
It automatically creates test cases for you, supports stateful testing, and works well with tools like pytest. You can use Hypothesis to improve your tests for anything from small scripts to complex applications.
In this guide, you'll learn what Hypothesis can do and how to use it to write and run property-based tests.
Prerequisites
Ensure you have Python installed—version 3.13 or higher. You should also know the basics of Python and have a general idea of how testing works.
Step 1 — Setting up the directory
Setting up a clean environment is essential before you start writing tests with Hypothesis. This helps keep your code organized and avoids conflicts with other Python packages you might have installed.
In this step, you'll create a new project folder, set up a virtual environment, and install Hypothesis.
To begin, create a new directory and navigate into it:
Then, create a virtual environment to isolate your project dependencies:
Activate the virtual environment:
Now, install Hypothesis:
With Hypothesis installed, you can add a small piece of code to test. Create a file called math_utils.py and add the following function:
The add() function takes two numbers and returns their sum. Save this file at the root of your project.
Next, let's write the first test using Hypothesis.
Step 2 — Writing your first test
Property-based tests help ensure that functions behave as expected under a wide range of inputs automatically generated by the testing framework. Instead of manually defining test cases, you'll define properties that should always hold true.
Create a file named test_math_utils.py at the root of your project and add the following test:
The @given decorator tells Hypothesis to generate test cases using the specified strategies. In this example, st.integers() generates random integer values for both a and b.
Within each test function, the assertions verify specific properties of the add function. The first test confirms that addition is commutative, while the second test ensures the function returns the correct sum.
Now that the test is written, let's run it in the next step.
Step 3 — Running your tests
With your test file set up, you can run it directly. Hypothesis tests are just normal Python functions decorated with @given, so you can run them by calling them.
Add the following code at the end of your test_math_utils.py file:
To execute the tests, run:
The output should look something like this:
By default, Hypothesis runs 100 test cases for each test function. If all the test cases pass, you'll see the "All tests passed!" message. If any test case fails, Hypothesis will display a detailed error message showing the failing input.
Step 4 — Test filtering and running specific tests
As your test suite grows, controlling which tests run and under what conditions becomes increasingly important. Hypothesis provides several ways to filter and focus your tests.
Using settings to configure test behavior
Hypothesis allows you to customize test behavior using the @settings decorator. Let's update our existing test_math_utils.py file to include this:
When you run this test, it will run the full 100 examples for the first two tests, but only 5 random examples for the new test:
Using example always to test specific values
Sometimes you want to ensure that certain specific values are always tested. You can do this with the @example decorator.
Let's add another test function to our file:
The highlighted code adds a new test function that uses @example decorators. These ensure that specific test cases (large numbers and negative/positive combinations) are always tested before the random cases, helping to catch edge cases that might be important for your function.
Run the updated file to see it in action:
Even though the output is the same, Hypothesis has now tested those specific examples first before moving on to the randomly generated ones. This is particularly useful for edge cases you want always to verify.
Using assume to filter test cases
Sometimes you want to test a property that only applies to certain inputs. Hypothesis provides assume to filter out test cases that don't meet specific conditions:
The newly added code demonstrates how to use assume() to skip test cases that don't meet certain conditions.
In this case, we're testing a mathematical property of integer division, but we need to avoid division by zero. The assume(b != 0) line tells Hypothesis to discard any test cases where b is zero and generate new ones instead.
Let's rerun the file to see the output:
Hypothesis has successfully generated 100 test cases for our division property, automatically skipping any where b was zero.
This approach allows you to focus on testing the property you care about without getting distracted by input values irrelevant to your testing.
Step 5 — Using built-in strategies
Hypothesis makes exploring test cases easy with its strategies API, which generates various test inputs. Let's explore some of the built-in strategies available.
Create a new file called validators.py with a function that checks if a string is a valid email address:
Now, create a test_validators.py test file that uses more complex strategies:
This example demonstrates several different strategies:
emails()- A built-in strategy that generates valid email addressestext()with a filter - Generates text strings that don't contain '@'lists(st.emails())- Generates lists of email addresses
Run the tests:
Oops! Our email validator doesn't handle all valid email formats that Hypothesis generates. This highlights the power of property-based testing: it finds edge cases we might not have thought to test manually.
Let's fix the validator by updating our regex pattern in validators.py:
The highlighted change updates the regular expression to support a wider range of valid email formats. Specifically, it now allows top-level domains (TLDs) with just a single character—like .c or .y—which are technically valid under email standards.
Previously, the pattern required TLDs to be at least two characters long (using {2,}), but that restriction has been removed. The new pattern uses * instead of {1,}, which means the domain can include one or more dot-separated parts, and each part only needs to match the allowed characters.
This makes the validator more flexible while enforcing a reasonable email address structure.
Now rerun the tests:
Great! Our improved validator now handles the edge cases that Hypothesis discovered. This demonstrates how Hypothesis helps you develop more robust code by automatically exploring a wide range of inputs, including those you might not have thought to test manually.
The built-in strategies in Hypothesis go far beyond what we've shown here. They include data types like:
integers(),floats(), and other numeric types with configurable rangestext()with many customization optionsdatetimes(),timezones(), and other time-related databinary()for byte strings- Container types like
lists(),dictionaries(), andsets() - And many more specialized strategies
Each strategy can be configured with parameters to restrict the generated values to a specific range or format, allowing you to tailor the test inputs to your needs.
Step 6 — Integrating with pytest
While running Hypothesis tests directly works well for small projects, most Python developers use testing frameworks like pytest for larger codebases. Hypothesis integrates seamlessly with pytest, providing enhanced reporting and organization features.
First, install pytest:
Let's create a new test_pytest_math.py file for our pytest tests. We'll start with simple tests similar to what we've already written, but in a pytest-compatible format:
Notice that you don't need a if __name__ == "__main__" block or explicit function calls. pytest will automatically discover and run any function with a name starting with test_.
Run the tests with pytest:
The -v flag turns on verbose mode, showing each test name as it runs.
pytest also gives you clear, structured output:
- A test session summary with total tests and duration
- Full test names with module paths
- Green “PASSED” messages for quick visual feedback
- A progress bar showing test completion
This makes it easier to track what’s running and spot issues—especially as your test suite grows.
Using parametrize with Hypothesis
pytest's parametrize decorator can be combined with Hypothesis to test multiple scenarios.
Create a test_pytest_parametrize.py file in the root directory:
In this example, parametrize defines different scenarios to test—like checking if addition is commutative or verifying your custom add() function behaves like Python’s built-in operator. Then Hypothesis steps in to generate 100 random input pairs (a, b) for each scenario.
This approach gives you a lot of coverage with very little code, helping you catch edge cases and logic errors without manually writing dozens of separate test cases.
Run the test:
Each line represents a different scenario from your parametrize, tested thoroughly with data from Hypothesis.
Running failed tests with --hypothesis-show-statistics
You can get more insight into what Hypothesis is doing by using the --hypothesis-show-statistics flag:
This gives you insights into how many examples were tried, how many failed, how long they took to run, and why Hypothesis stopped generating examples.
Using Hypothesis with pytest gives you the best of both worlds—pytest's flexible testing features and Hypothesis's smart, data-driven test generation. This pairing lets you write cleaner tests that cover more scenarios and catch more bugs with less code.
Final thoughts
Hypothesis makes it easier to catch edge cases by generating smart, varied inputs automatically. Instead of writing individual test cases, you define the rules your code should always follow.
Combined with pytest, Hypothesis gives you powerful, readable tests with less effort and better coverage. To explore more advanced features, check out the official Hypothesis documentation.