Hypothesis & Fuzzing

Composing Strategies with flatmap and @composite

You need property-based testing with Hypothesis to generate data whose parts are not independent: a list and a valid index into it, a date range whose end is after its start, a discriminated union whose payload depends on its tag. Drawing the parts separately produces nonsense — an index past the end of the list, an end date before the start. The fix is strategy composition: map, filter, flatmap, builds, and @st.composite each transform generation differently, and reaching for the wrong one is the difference between clean generation and a starved, health-check-tripping test. This guide draws the boundaries.

Prerequisites

Solution

All five combinators import from hypothesis.strategies (aliased st):

Python
from hypothesis import given, strategies as st

map — apply a pure function to one drawn value. No dependency between values.

Python
# Generate even integers by doubling any integer.
even_ints = st.integers().map(lambda n: n * 2)

@given(even_ints)
def test_even(n):
    assert n % 2 == 0

filter — reject drawn values that fail a predicate. Cheap only when the predicate passes most of the time; an over-strict filter starves the generator and trips a health check.

Python
# Fine: roughly half of integers pass.
nonzero = st.integers().filter(lambda n: n != 0)

@given(nonzero)
def test_nonzero(n):
    assert n != 0

flatmap — the key tool for interdependent values. flatmap(fn) draws a value, then calls fn(value) which must return a new strategy for the dependent part. Classic case: a list plus a valid index into it.

Python
# Draw a non-empty list, THEN draw an index that is valid for THAT list.
def list_and_index(xs):
    # xs is already drawn; build a strategy for a valid index into it.
    return st.tuples(st.just(xs), st.integers(min_value=0, max_value=len(xs) - 1))

list_with_valid_index = st.lists(st.integers(), min_size=1).flatmap(list_and_index)

@given(list_with_valid_index)
def test_index_in_bounds(pair):
    xs, i = pair
    assert xs[i] == xs[i]   # never IndexError: i depends on len(xs)

If you tried this with two independent strategies (st.tuples(st.lists(...), st.integers())), most generated indices would be out of bounds and you would be forced into an assume() that discards most examples. flatmap makes the dependency structural.

builds — construct an object by drawing each constructor argument independently. Ideal for flat objects with no cross-field constraints.

Python
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

points = st.builds(Point, x=st.integers(), y=st.integers())

@given(points)
def test_point(p):
    assert isinstance(p, Point)

@st.composite — imperative, multi-draw generation when several fields depend on each other. The decorated function receives a draw callable as its first argument; each draw(strategy) pulls a concrete value, and you combine them however you like.

Python
@st.composite
def date_range(draw):
    # draw() pulls concrete values; later draws can depend on earlier ones.
    start = draw(st.integers(min_value=0, max_value=10_000))
    span = draw(st.integers(min_value=0, max_value=365))
    end = start + span            # guaranteed end >= start by construction
    return (start, end)

@given(date_range())
def test_range_ordered(r):
    start, end = r
    assert end >= start

A discriminated union — payload depends on the tag — is the canonical @composite shape, and shows why it beats flatmap once there are several interdependent draws:

Python
@st.composite
def event(draw):
    kind = draw(st.sampled_from(["login", "purchase"]))
    if kind == "login":
        payload = {"session": draw(st.uuids())}
    else:
        payload = {"amount": draw(st.integers(min_value=1, max_value=10_000))}
    return {"kind": kind, "payload": payload}

@given(event())
def test_event_shape(e):
    if e["kind"] == "purchase":
        assert e["payload"]["amount"] >= 1
Choosing a strategy combinator by dependency shape A matrix with one row per combinator and four capability columns. map and filter check the single-value column; flatmap checks the one-dependency column; builds checks the independent-fields column; composite checks the many-interdependent-draws column. A final column states what each is best for, so the checks form a diagonal from single-value transforms up to fully interdependent generation. Which combinator: what dependency can it express? combinator single value one dependency independent fields many inter- dependent best for .map() pure transform of a drawn value .filter() rare rejection (else it starves) .flatmap() a value plus one derived from it st.builds() flat object, no cross-field rules @st.composite fields that reference each other
Each combinator expresses exactly one shape of dependency; the checks fall on a diagonal from single-value transforms up to fully interdependent draws. Pick the row that matches how your values relate, not the most powerful tool.

The difference between the two is where the dependency between draws is expressed.

flatmap and composite side by side Two panels comparing flatmap and the composite decorator: how each expresses a dependent draw, how readable a three-step dependency is in each, and how each behaves during shrinking. flatmap and composite side by side flatmap one dependency, inline value then strategy nests badly past two shrinks the outer first @composite many draws, imperative draw() reads like code stays flat at any depth shrinks each draw Both build one strategy object; neither costs anything at test time.
Use flatmap for a single dependent step and @composite the moment a second draw depends on the first.

Why this works

map and filter are single-value transforms — one in, one out — so they cannot express that a second value depends on a first. flatmap closes that gap by handing you the drawn value and letting you return a tailored strategy, which is exactly enough for one dependency. @st.composite generalizes this to arbitrarily many interdependent draws via repeated draw() calls, and because Hypothesis records every draw, it can still shrink a composite example to its minimal failing form. builds is the optimized path for the independent case and should be preferred over @composite when no field references another, because it lets Hypothesis use its internal flat generators.

Edge cases and failure modes

  • filter starvation. A predicate that rejects most candidates raises HealthCheck.filter_too_much; move the constraint into generation (bounded strategy or @composite) instead. See fixing Hypothesis FlakyHealthCheck failures.
  • flatmap shrinks worse than @composite. Deeply nested flatmap chains can shrink poorly; for three or more interdependent values, prefer @composite, which Hypothesis shrinks more effectively.
  • Calling draw() outside @composite. draw is only valid inside a @st.composite function; using it elsewhere raises an error. Conversely, never call a composite strategy's draw argument yourself — Hypothesis supplies it.
  • builds with interdependent fields. Using builds when fields must agree produces invalid objects; switch to @composite the moment one field's range depends on another's value.
  • map with impure functions. map should be a pure transform; side effects run on every generation and every shrink attempt, which is wasteful and can corrupt shared state.

Keeping composite strategies shrinkable

A composite strategy that generates good data but shrinks badly is worse than a simpler one, because the counterexample it reports is too large to read. Three habits keep shrinking effective.

Draw in dependency order and nothing else. Every draw() call is a decision Hypothesis can simplify, and it simplifies them roughly in the order they were made. A strategy that draws a size, then draws that many elements, shrinks the size first and the elements second, which produces the minimal failing input naturally. A strategy that draws the elements first and then derives a size from them gives the shrinker nothing to pull on.

Filter narrowly, and prefer construction over rejection. assume() and .filter() discard whole examples, and a filter that rejects most draws both slows generation and blocks shrinking, because the shrinker's smaller candidates keep getting rejected. Build the constraint into the draw instead:

Python
from hypothesis import strategies as st

# Rejection: most draws are thrown away, and shrinking stalls.
bad = st.tuples(st.integers(), st.integers()).filter(lambda t: t[0] < t[1])

# Construction: every draw is valid by design, and both values shrink freely.
@st.composite
def ordered_pair(draw, min_value=0, max_value=1000):
    low = draw(st.integers(min_value=min_value, max_value=max_value - 1))
    high = draw(st.integers(min_value=low + 1, max_value=max_value))
    return low, high            # invariant holds by construction, not by luck

Return plain data, not objects that hide their provenance. A composite that returns a fully built domain object is convenient, but when the test fails, Hypothesis prints the object's repr. If that repr is uninformative, the counterexample tells you nothing. Either give the object a faithful repr, or return the primitive draws alongside it so the report shows what was generated.

Python
@st.composite
def order_with_lines(draw):
    n = draw(st.integers(min_value=1, max_value=5))         # shrinks toward 1
    prices = draw(st.lists(st.integers(1, 10_000), min_size=n, max_size=n))
    # Returning both keeps the failing report readable.
    return {"line_count": n, "prices": prices}

The payoff is visible the first time a suite fails on real data: a well-ordered composite reports {"line_count": 1, "prices": [1]} where a rejection-heavy one reports a twelve-element list that happens to contain the trigger.

How the shrinker walks a composite A left-to-right flow of shrinking a composite strategy: the failing example is recorded, each draw is simplified in the order it was made, every candidate re-runs the property, and the first minimal example that still fails is reported. How the shrinker walks a composite failure recorded the full draw list simplify draw 1 earliest decision first re-run property candidate must still fail report minimum smallest failing draw A filter that rejects the simpler candidate stops the walk early.
Shrinking replays the property against progressively simpler draws, in the order the draws were made — which is why draw order is a design decision.

Frequently Asked Questions

When should I use flatmap instead of map in Hypothesis? Use map when the transformation is a pure function of one drawn value. Use flatmap when a later value depends on an earlier drawn value, because flatmap receives the drawn value and must return a new strategy for the dependent part.

What does the draw function do inside an @st.composite strategy?draw pulls a concrete value from another strategy at generation time. Calling draw multiple times lets you build interdependent values imperatively, and Hypothesis records every draw so it can shrink the composite example correctly.

How is builds() different from @composite?builds() constructs an object by drawing each argument from a strategy independently and calling the target, ideal for flat objects with no cross-field constraints. @composite is for imperative, interdependent generation where one field's strategy depends on another field's drawn value.

These combinators are the building blocks for generating custom strategies with hypothesis.strategies, where the same routing pattern keeps objects valid by construction. When a @composite chain slows a suite down, see reducing Hypothesis test execution time, and when an over-strict filter trips a health check, fixing Hypothesis FlakyHealthCheck failures covers the diagnosis. The discriminated-union @composite shape here also underpins modeling REST APIs as state machines, where each transition draws a payload conditioned on the chosen command.

← Back to Advanced Property-Based Testing