| name | maciver-hypothesis-testing |
| description | Test software in the style of David MacIver, creator of Hypothesis. Emphasizes sophisticated shrinking, example databases, stateful testing, and practical property-based testing in Python. Use when testing Python code with complex data structures, APIs, or stateful systems. |
| tags | property-based-testing, hypothesis, python, generators, shrinking, fuzzing, automated-testing, strategies |
David MacIver Hypothesis Style Guide
Overview
David MacIver created Hypothesis, widely considered the most sophisticated property-based testing library available. Hypothesis improves on QuickCheck with better shrinking (using internal reduction rather than type-based shrinking), an example database for regression testing, and deep integration with Python's ecosystem. MacIver's philosophy emphasizes that property-based testing should be practical, integrated into normal development workflows, and produce genuinely useful minimal examples.
Core Philosophy
"The purpose of Hypothesis is to make it easier to write better tests."
"Shrinking should produce the simplest example, not just a smaller one."
"Every failing example should be saved and replayed forever."
MacIver believes that property-based testing fails when it's treated as exotic. Hypothesis is designed to integrate seamlessly with pytest, produce human-readable minimal examples, and remember every failure to prevent regressions.
Design Principles
-
Integrated Shrinking: Shrinking happens during generation, not after—producing simpler examples.
-
Example Database: Every failure is saved and replayed on subsequent runs.
-
Compositional Strategies: Build complex generators from simple ones.
-
Practical by Default: Sensible defaults that work for real projects.
-
Stateful Testing: First-class support for testing state machines.
Hypothesis Architecture
┌─────────────────────────────────────────────────────────────┐
│ HYPOTHESIS INTERNALS │
├─────────────────────────────────────────────────────────────┤
│ │
│ STRATEGY (describes how to generate data) │
│ │ │
│ ▼ │
│ CONJECTURE ENGINE │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Generates a stream of bytes (the "choice sequence") │ │
│ │ │ │
│ │ Strategy interprets bytes → structured data │ │
│ │ │ │
│ │ Shrinking = finding smaller choice sequences │ │
│ │ that still fail (not type-aware, universal) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ EXAMPLE DATABASE │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ .hypothesis/examples/ │ │
│ │ Stores choice sequences for all failing examples │ │
│ │ Replays them first on every run │ │
│ │ Never forgets a failure │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
When Using Hypothesis
Always
- Use
@given decorator for property-based tests
- Let Hypothesis shrink—don't manually minimize
- Commit the
.hypothesis directory for CI
- Use
@example for important edge cases
- Combine with pytest fixtures naturally
- Use
assume() to filter invalid inputs
Never
- Ignore the example database (commit it!)
- Use
random directly—use strategies
- Catch exceptions to hide failures
- Set
max_examples too low (<100)
- Skip
@example for known edge cases
- Use
filter() when assume() works
Prefer
- Composite strategies over complex custom ones
@example for regression tests
assume() over filter() for preconditions
- Stateful testing for APIs
data() strategy for dynamic generation
- Settings profiles for CI vs local
Code Patterns
Basic Property Tests
from hypothesis import given, example, assume, settings
from hypothesis import strategies as st
@given(st.lists(st.integers()))
def test_sort_preserves_length(xs):
assert len(sorted(xs)) == len(xs)
@given(st.lists(st.integers()))
@example([])
@example([1])
@example([2, 1])
def test_sort_is_sorted(xs):
result = sorted(xs)
assert all(result[i] <= result[i+1] for i in range(len(result)-1))
@given(st.binary())
def test_compress_decompress_roundtrip(data):
assert decompress(compress(data)) == data
():
assume(b != )
(a // b) * b + (a % b) == a
Strategy Composition
from hypothesis import strategies as st
from hypothesis import given
@st.composite
def user_strategy(draw):
"""Generate valid User objects."""
name = draw(st.text(min_size=1, max_size=50))
age = draw(st.integers(min_value=0, max_value=150))
email = draw(st.emails())
return User(name=name, age=age, email=email)
@given(user_strategy())
def test_user_serialization(user):
assert User.from_json(user.to_json()) == user
def json_strategy():
"""Generate arbitrary JSON-compatible data."""
return st.recursive(
st.none() | st.booleans() | st.integers() | st.floats(allow_nan=False) | st.text(),
lambda children: st.lists(children) | st.dictionaries(st.text(), children),
max_leaves=50
)
@given(json_strategy())
def test_json_roundtrip(data):
import json
assert json.loads(json.dumps(data)) == data
@st.composite
def ():
a = draw(st.integers())
b = draw(st.integers(min_value=a))
(a, b)
():
a, b = pair
a <= b
Stateful Testing
from hypothesis.stateful import RuleBasedStateMachine, rule, invariant
from hypothesis import strategies as st
class DatabaseStateMachine(RuleBasedStateMachine):
"""
Test a key-value store against a reference model.
Hypothesis generates sequences of operations.
"""
def __init__(self):
super().__init__()
self.model = {}
self.db = Database()
@rule(key=st.text(), value=st.binary())
def put(self, key, value):
"""Put a key-value pair."""
self.model[key] = value
self.db.put(key, value)
@rule(key=st.text())
def get(self, key):
"""Get a value by key."""
model_result = self.model.get(key)
db_result = self.db.get(key)
assert model_result == db_result
@rule(key=st.text())
def delete(self, key):
"""Delete a key."""
self.model.pop(key, )
.db.delete(key)
():
(.model) == .db.size()
TestDatabase = DatabaseStateMachine.TestCase
():
():
().__init__()
.model = []
.queue = Queue()
items = Bundle()
():
.model.append(item)
.queue.push(item)
item
():
.model:
expected = .model.pop()
actual = .queue.pop()
expected == actual
():
expected = item .model
actual = .queue.contains(item)
expected == actual
Settings and Profiles
from hypothesis import settings, Verbosity, Phase, HealthCheck
from hypothesis import given
from hypothesis import strategies as st
@settings(
max_examples=500,
deadline=None,
suppress_health_check=[HealthCheck.too_slow],
)
@given(st.lists(st.integers(), min_size=1000))
def test_large_lists(xs):
assert sorted(xs) == sorted(xs)
settings.register_profile("ci", max_examples=1000)
settings.register_profile("dev", max_examples=100)
settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose)
@settings(deadline=200)
@given(st.binary(min_size=1000))
def test_with_deadline(data):
process(data)
@settings(
phases=[
Phase.explicit,
Phase.reuse,
Phase.generate,
Phase.shrink,
]
)
():
n <
Advanced Strategies
from hypothesis import strategies as st
from hypothesis import given
@given(st.fixed_dictionaries({
'name': st.text(min_size=1),
'age': st.integers(0, 150),
'active': st.booleans(),
}))
def test_user_dict(user):
assert 'name' in user
assert 0 <= user['age'] <= 150
@given(st.one_of(
st.none(),
st.integers(),
st.text(),
))
def test_nullable_values(value):
assert value is None or isinstance(value, (int, str))
from hypothesis import given
from hypothesis.strategies import data
@given(data())
():
n = data.draw(st.integers(min_value=, max_value=))
xs = data.draw(st.lists(st.integers(), min_size=n, max_size=n))
(xs) == n
():
color [, , ]
dataclasses dataclass
:
x:
y:
():
(point, )
(point, )
():
(point, Point)
Shrinking Examples
@given(st.lists(st.integers()))
def test_no_duplicates_bad(xs):
assert len(xs) == len(set(xs))
@given(st.integers().map(lambda x: x * 2))
def test_even_numbers(n):
assert n % 2 == 0
@given(st.integers().filter(lambda x: x > 10))
def test_large_numbers():
n <
Integration with Pytest
import pytest
from hypothesis import given, settings
from hypothesis import strategies as st
@pytest.fixture
def database():
db = Database()
yield db
db.close()
@given(key=st.text(), value=st.binary())
def test_database_roundtrip(database, key, value):
database.put(key, value)
assert database.get(key) == value
@pytest.mark.parametrize("operation", ["add", "subtract", "multiply"])
@given(a=st.integers(), b=st.integers())
def test_operations(operation, a, b):
result = calculate(operation, a, b)
if operation == "add":
assert result == a + b
@pytest.mark.slow
@settings(max_examples=10000)
@given(st.binary(min_size=10000))
def test_large_data(data):
process(data)
():
Reproducing Failures
from hypothesis import given, reproduce_failure, settings
from hypothesis import strategies as st
@reproduce_failure('6.100.0', b'AAAB')
@given(st.lists(st.integers()))
def test_reproduction(xs):
assert len(xs) == len(set(xs))
@settings(database=None)
@given(st.integers())
def test_with_seed(n):
assert n != 42
Testing APIs
from hypothesis import given, assume
from hypothesis import strategies as st
import requests
@st.composite
def http_request(draw):
method = draw(st.sampled_from(['GET', 'POST', 'PUT', 'DELETE']))
path = '/' + draw(st.text(
alphabet='abcdefghijklmnopqrstuvwxyz/',
min_size=1,
max_size=50
))
if method in ('POST', 'PUT'):
body = draw(st.dictionaries(
st.text(min_size=1),
st.text() | st.integers() | st.booleans()
))
else:
body = None
return {'method': method, 'path': path, 'body': body}
@given(http_request())
def test_api_doesnt_crash(request):
"""Property: API should never crash (return 5xx)."""
response = make_request(
request['method'],
request['path'],
json=request['body']
)
assert response.status_code < 500
@given(st.from_schema())
():
response = client.request(**request)
validate_response(response, openapi_schema)
Mental Model
MacIver approaches testing by asking:
- What properties should hold? Think invariants and roundtrips
- What's the simplest failure? Trust Hypothesis shrinking
- Am I saving failures? Commit the example database
- Can I compose strategies? Build complex from simple
- Is it stateful? Use RuleBasedStateMachine
The Hypothesis Checklist
□ Use @given with appropriate strategies
□ Add @example for known edge cases
□ Use assume() for preconditions
□ Commit .hypothesis/ directory
□ Set appropriate max_examples (100+)
□ Configure CI profile with more examples
□ Use composite strategies for complex types
□ Use stateful testing for APIs
□ Trust the shrinking—don't manually minimize
□ Check deadline settings for slow tests
Signature MacIver Moves
- Integrated shrinking (choice sequence reduction)
- Example database (never forget a failure)
- Composite strategies (@st.composite)
- Stateful testing (RuleBasedStateMachine)
- Settings profiles (CI vs dev)
- Type-based inference (st.from_type)
- Recursive strategies (st.recursive)
- Health checks for performance issues