A whirlwind tour of the decorators that let you describe what your code should do β and the fuzzer that takes those same decorators and spends hours hunting for cases where it doesn't.
An ordinary unit test asserts a fact about one specific input. A property test asserts a fact about all inputs of a certain shape, and lets Hypothesis manufacture the inputs for you.
from hypothesis import given
from hypothesis import strategies as st
@given(st.lists(st.integers()))
def test_sort_idempotent(xs):
assert sorted(sorted(xs)) == sorted(xs)
Two pieces. The @given decorator promotes a regular function into a property test. Its argument is a strategy β a recipe for generating values. st.lists(st.integers()) reads as lists of integers, of varying lengths and varying contents. Hypothesis runs the function many times β about 100 by default β and each run walks through the loop above: DRAW a list, INVOKE the function, ASSERT the property, and (if anything misbehaves) SHRINK.
Strategies compose. You build complex generators from simple ones the way you build complex types from simple ones. Try drawing some yourself β pick a strategy, then click DRAW a few times and watch how the values cluster around boundaries:
You'll notice the values are weird. Empty strings. Zero. The smallest negative integer your platform supports. Surrogate-pair regions of Unicode. That's not random β Hypothesis biases hard toward edge cases on purpose. Real bugs cluster at the boundaries, and the strategies know it.
This is where the tutorial earns its keep. Here's a function that looks right and almost is:
def dedupe_sort(xs):
# sort and remove duplicates
return sorted(set(xs))
And here's the property we'll check: the result has the same length as the input. The function violates this property β but only on inputs that contain duplicates. Watch what each tier of testing does with it.
Round one passes β every hand-picked input happened to have unique values, so the bug was invisible. Round two finds it: random generation stumbles onto a duplicate within the first hundred draws, and shrinking carves the failure down to a two-element list. Round three is where the conversation shifts. With enough time and coverage feedback, HypoFuzz can stretch into corners the random generator visits only by accident β rare branches, type combinations, latent assumptions about input distribution.
When Hypothesis catches a failure, it doesn't just hand you the offending input. It hands you the simplest input it can find that still fails. The replay below shows what that looks like β an arbitrary 8-element list, whittled down step by step to the irreducible minimum:
Shrinking is what makes property tests debuggable. A 47-element list with the failure buried inside is operationally useless; a two-element list [0, 0] tells you instantly what the problem is. The shrinker is generic β Hypothesis knows how to reduce any value any strategy can produce, because the strategies and the shrinker are two faces of the same machinery.
Here is the move. HypoFuzz reads your existing @given tests β the same files, the same decorators, no rewrites β and runs them continuously, with a coverage feedback loop bolted onto the side of the standard Hypothesis loop.
~100 examples per test. Finishes in seconds. Stops.
Millions of examples. Steers toward unexplored branches. Runs as long as you let it.
The coverage loop is the trick. After every example, HypoFuzz observes which lines and branches the function exercised. Inputs that hit something new get prioritized β saved as seeds, mutated, returned to. Inputs that retread known ground fall down the queue. Over hours and days, the coverage map fills in:
Each cell stands for a code path. Amber lights up quickly β these are the easy paths a normal CI run would already reach. Red is the deeper material: rare branches, error handlers, weird state combinations. A regular pytest invocation rarely lights up the red cells. That's where the unexplored bugs live.
The cost of all this? Almost nothing on your end. The decorators, the strategies, the properties β all written. HypoFuzz inherits them. The only new artifact is a dashboard you check on Monday morning to see what shook loose over the weekend.
If you walked the loop with me, you wrote three things and got four. The three: a strategy (what shape are my inputs?), a property (what stays true?), and the function under test. In return you have example tests, plus a generator, plus a shrinker, plus a fuzzing target β all from the same six lines.
That asymmetry β small surface, large yield β is the entire pitch. @given is the smallest interface that lets a tool do this much work for you. HypoFuzz is what happens when you let that work run for longer than your CI budget.