Last modified: Oct 24, 2026
Property-Based Testing with Hypothesis
Unit tests check specific examples. You pass 2 and 3, and you expect 5. That works well. But what about 0, -1, or a million? You cannot write every case by hand.
Property-based testing flips this idea. Instead of examples, you describe rules that must always hold. A library then generates many inputs and checks your rules.
Hypothesis is the most popular property-based testing library for Python. This guide explains how it works, why it matters, and how to use it well.
What Is Property-Based Testing?
A property is a statement that should be true for all valid inputs. For example, reversing a list twice gives back the original list. That is a property.
Example-based tests check one input. Property-based tests check hundreds of inputs automatically. The library tries to break your property.
When it finds a failure, it does not stop there. It shrinks the input. Shrinking means it searches for the smallest input that still fails.
Small failing inputs are easier to debug. You get a clear counterexample instead of a messy random one.
Why Use Hypothesis?
Hypothesis finds edge cases you would never think of. Empty strings, zero values, huge numbers, and unicode characters all get tested.
It also saves time. One property can replace dozens of hand-written tests. Your test suite becomes smaller and stronger.
If you already create fake data in tests, property-based testing goes further. It explores the input space instead of fixing values. You can still learn how to generate test data with Faker in Python for example-based tests, and combine both approaches.
Installing Hypothesis
Installation is simple. Use pip to add it to your environment.
pip install hypothesis
Hypothesis works with pytest and unittest. Most people use it with pytest, so install that too if needed.
Your First Property Test
Let us start with a simple function. It sorts a list of numbers.
def sort_numbers(numbers):
return sorted(numbers)
Now we write a property test. We use the @given decorator to describe the input type.
from hypothesis import given
from hypothesis import strategies as st
@given(st.lists(st.integers()))
def test_sort_keeps_length(numbers):
# Sorting must never change the number of items
assert len(sort_numbers(numbers)) == len(numbers)
@given(st.lists(st.integers()))
def test_sort_returns_sorted_list(numbers):
result = sort_numbers(numbers)
# Every item must be less than or equal to the next one
assert all(result[i] <= result[i + 1] for i in range(len(result) - 1))
Hypothesis generates many lists. They include empty lists, negative numbers, and duplicates. If any list breaks a property, the test fails.
$ pytest test_sort.py
================== test session starts ==================
collected 2 items
test_sort.py .. [100%]
=================== 2 passed in 0.42s ===================
Understanding Strategies
Strategies describe how to build inputs. The module st holds many ready-made strategies.
st.integers() produces whole numbers. st.text() produces strings. st.lists() wraps another strategy and builds lists.
You can also combine strategies. st.one_of() picks from several options. st.tuples() builds fixed-size groups.
Strategies are the heart of Hypothesis. Good strategies mean good tests.
A Real Example: Encoding and Decoding
Properties shine with round-trip checks. If you encode data and decode it, you should get the original back.
import json
from hypothesis import given
from hypothesis import strategies as st
@given(st.dictionaries(st.text(), st.integers()))
def test_json_round_trip(data):
# Encoding then decoding must return the same dict
encoded = json.dumps(data)
decoded = json.loads(encoded)
assert decoded == data
This test checks many dictionaries. Empty ones, ones with unicode keys, and ones with large values all get covered.
Round-trip properties also fit serialization formats. If you compile and use .proto files in Python, you can test that messages survive a serialize and parse cycle.
How Shrinking Helps You Debug
Suppose a property fails on a long list with odd numbers. Hypothesis will not show you that list. It will shrink the input automatically.
It tries shorter lists, simpler values, and smaller numbers. The final report shows the simplest failing case.
from hypothesis import given
from hypothesis import strategies as st
@given(st.lists(st.integers()))
def test_sum_is_positive(numbers):
# This property is wrong on purpose
assert sum(numbers) > 0
Falsifying example: test_sum_is_positive(
numbers=[],
)
The smallest failing input is an empty list. The sum is zero, so the property fails. That is much clearer than a random list of 50 numbers.
Controlling Test Settings
Hypothesis runs 100 examples by default. You can change this with the @settings decorator.
from hypothesis import given, settings
from hypothesis import strategies as st
@settings(max_examples=500)
@given(st.integers(min_value=1, max_value=1000))
def test_square_is_larger(n):
# For numbers above one, the square is bigger
assert n * n >= n
More examples mean more coverage but slower tests. Pick a number that balances speed and confidence.
You can also set deadlines. A deadline catches slow inputs before they slow your whole suite.
Common Pitfalls to Avoid
Do not test trivial properties. A property like "the function returns something" proves little. Aim for rules that reflect real behavior.
Avoid using random values inside your test body. Let strategies control inputs. This keeps shrinking reliable.
Do not ignore failing examples. Hypothesis saves them in a database. Fix the bug, and the suite stays green.
Keep properties simple. If a property is hard to explain, split it into smaller ones.
When to Use Property-Based Testing
It fits pure functions best. Sorting, parsing, encoding, and math all work well. These functions have clear input and output rules.
It also helps with parsers and data pipelines. If you process text, you can test that token counts never exceed character counts. That pairs well with tokenization with NLTK in Python, where round-trip and length rules are common.
It is less useful for code with heavy side effects. Network calls and database writes are harder to model as properties. You can still test the pure parts around them.
Combining Hypothesis with Other Tools
Hypothesis plays well with pytest fixtures. You can build clients once and pass them into property tests.
For example, you can test a message producer against a fake broker. If your app uses Kafka, you can produce and consume Kafka messages in Python and check that payloads survive the trip.
You can also test API wrappers. With OpenAI function calling in Python, a property could check that every generated JSON argument matches the declared schema.
Best Practices
Start small. Write one property for one function. Run it and watch it pass.
Name tests after the property, not the input. "Test reverse twice" is better than "test list".
Use assume() to skip invalid inputs. It tells Hypothesis to ignore cases that do not fit your rule.
Review failing examples carefully. They often reveal real bugs in code you trusted.
Conclusion
Property-based testing with Hypothesis gives you stronger tests with less code. You describe rules, and the library hunts for inputs that break them.
Shrinking turns failures into small, clear examples. Strategies let you model inputs precisely. Settings let you tune speed and coverage.
Start with one simple property today. Reverse a list, sort numbers, or round-trip JSON. Once you see a bug found automatically, you will not want to go back.