Traditional unit testing checks expected inputs and outputs. But what if you could test your functions against a wide range of generated inputs, including edge cases you never thought of? That’s where property-based testing comes in — and in Python, the go-to tool is the powerful library Hypothesis.
This guide walks you through what property-based testing is, how Hypothesis works, and how to use it effectively in real-world Python code.
What is Property-Based Testing?
Property-based testing is a method where tests are written as rules or properties your code should always satisfy, regardless of input. Instead of writing dozens of test cases manually, the framework generates inputs automatically and tests whether your code holds up.
Example: Property
The reverse of a reversed list should be the original list.
No need to test [1, 2, 3]
, [0]
, or []
separately. Hypothesis will generate all kinds of lists — including edge cases you wouldn’t even consider.
Why Use Hypothesis?
- Test with more coverage and fewer lines of code
- Uncover edge-case bugs effortlessly
- Fewer missed cases compared to manually written tests
- Works seamlessly with
pytest
andunittest
Installing Hypothesis
Install with pip:
pip install hypothesis
Or with pytest support:
pip install hypothesis pytest
Basic Example
Let’s test this property: Reversing a list twice gives the original list.
from hypothesis import given from hypothesis.strategies import lists, integers @given(lists(integers())) def test_reverse_identity(xs): assert list(reversed(list(reversed(xs)))) == xs
Hypothesis will automatically:
- Generate random lists of integers
- Run this test hundreds of times
- Shrink inputs to the smallest failing case if it fails
Common Hypothesis Strategies
Strategy | Description |
---|---|
integers() |
Any integer |
text() |
Unicode strings |
lists(...) |
Lists of elements |
dictionaries() |
Dicts with keys and values |
booleans() |
True/False |
emails() |
Valid email strings (via hypothesis.extra ) |
one_of(...) |
Randomly chooses from provided strategies |
You can combine and customize them for complex data.
Real-World Example: Email Validator
Let’s say you wrote a function to validate email addresses:
import re def is_valid_email(s: str) -> bool: return re.match(r"[^@]+@[^@]+\.[^@]+", s) is not None
Now test it with property-based testing:
from hypothesis import given from hypothesis.strategies import text @given(text()) def test_email_does_not_crash(s): is_valid_email(s)
This ensures your function never throws on any kind of text input — even bizarre Unicode strings
Custom Strategies
You can define custom input strategies using @composite
:
from hypothesis.strategies import composite @composite def user_names(draw): first = draw(text(min_size=1, max_size=10)) last = draw(text(min_size=1, max_size=10)) return f"{first}_{last}"
Use it in your test:
@given(user_names()) def test_username_has_underscore(name): assert "_" in name
Shrinking: Finding Minimal Failing Cases
When a test fails, Hypothesis shrinks the input to find the smallest example that still fails. This makes debugging much easier.
Best Practices
- Use clear, high-level properties
- Combine with example-based tests
- Limit input size if performance matters
- Use
.filter()
to constrain inputs - Avoid stateful or side-effect-heavy code
Conclusion
Hypothesis transforms the way you test by generating smart, varied inputs that help uncover bugs traditional tests miss. Whether you’re building a utility function or a critical backend service, property-based testing improves confidence and code quality.
Start simple, think in properties, and let Hypothesis do the heavy lifting.
Keep Coding…