Reusable, shrink-friendly Hypothesis
strategies for the domains where hand-rolling st.* goes wrong.
You reach for property-based testing, and within ten minutes you're yak-shaving
a strategy for "a valid timezone-aware datetime that isn't stuck in a DST gap",
or "money as a Decimal with the right number of cents", or "a GeoJSON polygon
whose ring is actually closed". Get it subtly wrong and you either miss bugs
(your generator never produces the nasty input) or drown in false failures (your
generator produces invalid input your code was right to reject). hypothesis-recipes
is a library of those strategies, already debugged, already shrinking cleanly.
from hypothesis import given
from hypothesis_recipes import money_amounts, bounding_boxes, ipv4_addresses
@given(money_amounts(currency="JPY"))
def test_no_fractional_yen(amount):
assert amount == amount.to_integral_value() # JPY has 0 minor units
@given(bounding_boxes())
def test_box_is_well_formed(bbox):
west, south, east, north = bbox
assert west <= east and south <= north # never inverted
@given(ipv4_addresses())
def test_parses(addr):
import ipaddress; ipaddress.ip_address(addr) # always validSample draws from examples/demo.py:
money.money (amount, currency):
["2026.67", "GBP"]
["3.865", "KWD"]
["254748", "JPY"]
geo.geojson_points:
{"type": "Point", "coordinates": [125.766..., -72.375...]}
net.urls:
"http://sl9hti-ze0nirvc7x-b84x7u-f/iO9AArBz/b/Hgyck/1?xhc9=So&1e100=f"
The hard-won lesson of property-based testing is that the strategy is where the bugs hide — both the bugs in your code and the bugs in your test. Two failure modes dominate:
- Generating invalid data. A latitude of
95.0, a datetime at02:30on a spring-forward night, a bounding box withmin > max. Your code correctly rejects it, Hypothesis reports a "failure", and you've learned nothing. - Leaning on
.filter(). The lazy fix for (1) is to generate junk and filter it out. That shrinks poorly, and Hypothesis will eventually raiseFailedHealthCheckwhen it can't find enough valid examples.
Every recipe here is written constructively instead: a valid IP is built
from an integer via ipaddress, a bounding box sorts its own corners, a
business-day date is drawn as an index into the sequence of weekdays. Nothing is
filtered, so everything shrinks to a small, readable counterexample. If you want
the theory behind writing your own, the site has a walkthrough of
building custom strategies that shrink well.
hypothesis-recipes is a small pure-Python package. Its only dependency is
hypothesis itself. It is meant to be used from a clone — add the checkout
to your PYTHONPATH and import the strategies.
git clone https://github.com/python-testing-debugging/hypothesis-recipes.git
cd hypothesis-recipes
python -m pip install hypothesis # the one runtime dependency
# use it from your own project by pointing PYTHONPATH at the checkout:
PYTHONPATH=/path/to/hypothesis-recipes python -m pytestThen, in your tests:
from hypothesis_recipes import dates, money, geo, json_api, text, net
# or grab specific strategies from the top level:
from hypothesis_recipes import aware_datetimes, slugs, json_values| Module | What it generates |
|---|---|
hypothesis_recipes.dates |
naive/aware datetimes, DST-safe datetimes, date ranges, business days |
hypothesis_recipes.money |
Decimal amounts with exact minor units, currency codes, (amount, ccy) |
hypothesis_recipes.geo |
latitudes/longitudes, bounding boxes, GeoJSON Point/Polygon |
hypothesis_recipes.json_api |
recursive JSON, schema objects, ISO-8601 stamps, pagination, resources |
hypothesis_recipes.text |
slugs, emails, usernames, identifiers, "nasty" unicode strings |
hypothesis_recipes.net |
IPv4/IPv6 addresses, ports, hostnames, URLs |
Every public strategy is a function returning a SearchStrategy, takes
keyword-only knobs with sane defaults, is fully type-hinted, and carries a >>>
example in its docstring.
The whole library is an exercise in one principle — construct, don't filter — applied per domain:
- Bounded integers → valid objects. IP addresses come from
st.integers(0, 2**32-1).map(IPv4Address); business days from an integer index into the Mon–Fri sequence; money from an integer count of minor units scaled by10**-places. Integers shrink toward zero, so counterexamples are minimal and readable (0.0.0.0, the earliest weekday,Decimal("0.00")). - Sort/close/clamp in a
@composite. Bounding boxes draw two lats and two lons and sort them; GeoJSON polygons sample points around a circle and repeat the first as the last to close the ring. The result is always well-formed. - Convert through an instant for time. DST-safe datetimes generate a UTC
instant and
astimezoneit into a real zone — a converted instant can never land in a spring-forward gap or be an ambiguous fall-backfoldtime. - Compose the built-ins. Where a plain composition of
st.*already shrinks well (JSON viast.recursive, schema objects viast.fixed_dictionaries), the recipe just wires it up with good defaults.
The test suite pins a deterministic Hypothesis profile (derandomize=True, no
deadline) so runs are reproducible and CI stays green — see the notes on
keeping Hypothesis suites fast.
Draw a handful of examples from every strategy in one command:
PYTHONPATH=. python examples/demo.pyOr run the living-documentation property tests, which use the recipes to check
real invariants (money round-trips through cents, JSON round-trips through
json.dumps, every URL parses with urllib.parse):
PYTHONPATH=. python -m pytest examples/test_examples.pyThese recipes are companions to the property-based testing material at python-testing-debugging.com. If you want to go from using strategies to designing your own, start here: