Testing in Practice · Foundations: Strategy, pytest and Test Doubles
Property-Based Testing with Hypothesis
Let Hypothesis generate hundreds of inputs to check properties that must always hold, find edge cases you didn't think of, and shrink failures.
What you'll build
Property tests for two functions: a price-splitting function where Hypothesis finds an input that loses a cent and shrinks it to a minimal example, and a round-trip property (encode then decode returns the original) that passes for thousands of generated strings.
The idea
- Instead of picking examples, describe properties: "the parts always add up to the total", "decode(encode(x)) == x", "sorting twice equals sorting once".
- Hypothesis generates many inputs (including edge cases like empty strings, zero, huge numbers, Unicode) and, on failure, shrinks to the simplest failing input.
- Failing examples are saved and replayed; add them as regular tests with
@example.
The code
# file: money.py
def split_evenly(total_cents: int, people: int) -> list[int]:
share = total_cents // people
return [share] * people # bug: the remainder disappears
def split_fairly(total_cents: int, people: int) -> list[int]:
share, remainder = divmod(total_cents, people)
return [share + (1 if i < remainder else 0) for i in range(people)]
def encode(text: str) -> str:
return text.encode("utf-8").hex()
def decode(data: str) -> str:
return bytes.fromhex(data).decode("utf-8")
# file: test_money.py
from hypothesis import given, settings, strategies as st
from money import decode, encode, split_evenly, split_fairly
amounts = st.integers(min_value=0, max_value=10_000_000)
people = st.integers(min_value=1, max_value=50)
@settings(derandomize=True, database=None)
@given(amounts, people)
def test_even_split_adds_up(total, n):
assert sum(split_evenly(total, n)) == total
@settings(derandomize=True, database=None)
@given(amounts, people)
def test_fair_split_adds_up_and_differs_by_at_most_one_cent(total, n):
parts = split_fairly(total, n)
assert sum(parts) == total
assert max(parts) - min(parts) <= 1
@settings(derandomize=True, database=None, max_examples=500)
@given(st.text())
def test_encode_round_trips(text):
assert decode(encode(text)) == text
# file: main.py
import pytest
pytest.main(["-q", "--tb=short", "-p", "no:cacheprovider"])
Output
F.. [100%]
=================================== FAILURES ===================================
___________________________ test_even_split_adds_up ____________________________
test_money.py:8: in test_even_split_adds_up
@given(amounts, people)
^^^
test_money.py:10: in test_even_split_adds_up
assert sum(split_evenly(total, n)) == total
E assert 0 == 1
E + where 0 = sum([0, 0])
E + where [0, 0] = split_evenly(1, 2)
E Failing test case: test_even_split_adds_up(
E total=1,
E n=2,
E )
=========================== short test summary info ============================
FAILED test_money.py::test_even_split_adds_up - assert 0 == 1
1 failed, 2 passed in <time>Run this on your computer — see “Get Started” for setup.
Hypothesis reduced the failure to the smallest case (total=1, n=2): one cent split between two people disappears.
Common mistakes
- Restating the implementation as the property (you'll just reproduce the bug); test invariants instead.
- Strategies that are too narrow (only small positive numbers), missing the edge cases that matter.
- Ignoring the shrunk counterexample instead of turning it into a permanent regression test.
Key points
- Describe properties that must always hold.
- Hypothesis generates edge cases and shrinks failures to minimal examples.
- Keep found counterexamples as regular tests.
Further reading: Hypothesis documentation · What is property-based testing?
Practice questions
What is a property in property-based testing?
Answer
A statement that must hold for all valid inputs, such as 'the parts sum to the total' or 'decode(encode(x)) == x'.
What does shrinking do?
Answer
When a test fails, Hypothesis simplifies the failing input step by step to the smallest example that still fails.
Which kinds of code benefit most from property-based tests?
Answer
Parsers, serializers, money and date calculations, data transformations and anything with round trips or invariants.
Open in the app to mark lessons complete, take notes and track your progress.