Quant Memo
Core

Property-Based Testing

Instead of checking a function against a handful of examples you thought of, property-based testing generates hundreds of random inputs and checks that a general rule always holds, which finds the edge cases a human wouldn't think to write by hand.

Prerequisites: Unit Testing Fundamentals

A hand-written unit test checks a function against specific numbers you thought of — usually the typical case and one or two edge cases you happened to remember. That leaves a huge space of inputs untested, and bugs love to hide in exactly the combinations nobody thought to try: a negative size, a price of zero, a portfolio with one asset instead of several. Property-based testing flips the approach — instead of picking specific inputs, you state a property that should hold for every valid input, and a library generates hundreds of random inputs (including deliberately weird ones) to try to break it.

The idea: describe a rule, let the machine hunt for counterexamples

A property is something that should always be true regardless of the specific numbers — "sorting a list twice gives the same result as sorting it once," "a portfolio's weights should always sum to 1 after normalization," "encoding then decoding a message returns the original message." The testing library generates random inputs matching a type or shape you specify, runs the function, and checks the property on every one; if it finds a case where the property fails, it automatically shrinks that case down to the smallest input that still fails, so you get a minimal, readable counterexample instead of a huge random one.

from hypothesis import given, strategies as st

def normalize_weights(weights):
    total = sum(weights)
    return [w / total for w in weights]

@given(st.lists(st.floats(min_value=0.01, max_value=1e6), min_size=1, max_size=20))
def test_weights_sum_to_one(weights):
    result = normalize_weights(weights)
    assert abs(sum(result) - 1.0) < 1e-9

Worked example: finding a bug that examples missed

normalize_weights looks correct and passes every hand-written example: [1, 1, 1] -> [0.333, 0.333, 0.333] sums to 1, [10, 20, 30] -> [0.166, 0.333, 0.5] sums to 1. But Hypothesis, generating random lists, eventually tries something like [1e6, 0.01] — a huge value next to a tiny one — and finds that floating-point division introduces enough rounding error that the sum drifts to 0.9999999991, failing the 1e-9 tolerance. It then shrinks the failing example, discarding elements and simplifying values while the failure persists, until it reports something minimal like [1000000.0, 0.01] instead of the original random list of twenty numbers — pointing straight at the numerically unstable case: extreme ratios between weights.

Falsifying example: test_weights_sum_to_one(
    weights=[1000000.0, 0.01],
)
AssertionError: assert abs(0.9999999990999999 - 1.0) < 1e-9
hand-written examples found failure
Hand-written tests cluster near "typical" inputs. Randomised generation spreads across the whole input space and is far more likely to land on the extreme case that breaks the property.

The value of property-based testing is in the properties, not the randomness alone: "output always sums to 1," "function is idempotent," "round-tripping returns the original" are properties a human can state confidently even without knowing every input in advance — which is exactly what makes them checkable across hundreds of generated cases instead of a handful of guessed ones.

Where it shows up

Interviewers occasionally ask you to state a property for a function you just wrote as a sanity check — "what's true about this output no matter the input?" is a quick way to test whether you understand your own code's contract, independent of whether you use a property-testing library at all.

In production quant systems, property-based testing earns its keep on exactly the kind of numerically finicky code hand examples tend to under-test: portfolio weight normalization, position sizing under leverage constraints, order book invariants (bid always below ask), and serialization round-trips for market data messages. It pairs naturally with Numerical Tolerance in Tests, since floating-point properties almost always need an explicit tolerance rather than exact equality.

A property that's too weak to fail on a real bug is worse than no test — "output is a list" is technically a property but catches almost nothing. Spend the effort on properties that encode an actual invariant of the system (conservation of total weight, monotonicity, round-trip equality), not on trivially-true statements.

Related concepts

Practice in interviews

Further reading

  • MacIver & Hypothesis contributors, Hypothesis documentation
ShareTwitterLinkedIn