Property-based testing checks a general rule across many generated inputs, rather than specifying a separate expected answer for every case. In quant software, it can expose a payoff sign error, a broken accounting identity or a transformation that loses data across inputs the author did not think to write down individually.
A property-based test needs two things: an input domain and a statement that should hold throughout that domain. It can test a small function as a unit test, or exercise a larger system. The term describes how cases and expectations are specified; it does not determine the size of the component under test.
Start with a contract, then generate cases
Consider the terminal payoffs of a call option and a put option with the same strike. Let be the underlying price at expiry and the strike, both in USD per underlying unit. The payoffs per unit are
Two useful properties are nonnegativity and the terminal identity
When , only the call can have a positive payoff; when , only the put can. These two cases explain the identity algebraically. This is an identity between expiry cash flows, not a test of discounted put–call parity between option prices before expiry.
The following deliberately small domain uses nonnegative integer prices from 0 to 200 inclusive. Integers make equality exact and keep floating-point tolerances out of the example. The four corners are explicit checks; the generator adds 1,000 pairs, which may include duplicates. Inputs are fictional.
from random import Random
def payoffs(S: int, K: int) -> tuple[int, int]:
"""Return call and put terminal payoffs per underlying unit."""
return max(S - K, 0), max(K - S, 0)
anchors = {
(0, 0): (0, 0),
(0, 200): (0, 200),
(200, 0): (200, 0),
(200, 200): (0, 0),
}
rng = Random(2026)
cases = list(anchors) + [
(rng.randint(0, 200), rng.randint(0, 200))
for _ in range(1000)
]
for S, K in cases:
call, put = payoffs(S, K)
assert call >= 0 and put >= 0, (S, K, call, put)
assert call - put == S - K, (S, K, call, put)
for inputs, expected in anchors.items():
assert payoffs(*inputs) == expected
print(f"{len(cases)} cases passed")
# 1004 cases passedThis standard-library illustration was run with Python 3.14.4, with assertions enabled. Run it without
-O, which removes these assertions. A local Random instance avoids changing the module's shared generator state. Record the runtime as well as the seed when preserving a failure: Python does not promise identical randint sequences across all future versions. See the official random-number documentation and assert statement reference.Passing these cases establishes what happened on those inputs. It does not establish correctness for all prices, fractional prices, missing data or a production pricing engine. Those require their own contract and tests. For floating-point arithmetic, choose tolerances that reflect the scale and numerical method; do not copy exact integer equality without considering the change of domain.
A property can be too weak
Suppose a faulty implementation adds USD 1 to both terminal payoffs. They remain nonnegative, and the extra amounts cancel in their difference. Both generated properties still pass for every pair in this example. More generated cases cannot uncover that particular fault through those properties alone.
At expiry with spot equal to strike, both payoffs must be zero. The independent known answer detects a common error that the relationship misses.
An oracle is the reference used to decide whether an answer is correct. Combine algebraic properties with independently established examples, such as the zero payoffs at equal spot and strike. Recomputing the implementation's expression inside the test can repeat the same mistake. Also check whether your suite rejects a deliberately broken implementation: it is useful evidence that the chosen tests can detect the fault you care about.
What a library adds
Hypothesis is a Python property-based testing library. Its strategies describe allowed inputs, and
@given supplies generated cases to a test function. Its search is aimed at finding bugs; its generated inputs should not be interpreted as a representative sample of market conditions or as a statistical confidence interval. The official domain and distribution explanation makes that distinction explicit.Hypothesis can also shrink a generated failing case: it searches for a simpler input that still fails, making diagnosis easier. A shrunk counterexample is not a proof that every smaller input passes. See its glossary of failing and minimal failing cases. The code above demonstrates the underlying testing idea with a seeded loop; it does not implement shrinking or claim a Hypothesis run.
When a generated case reveals a real defect, preserve a concrete failing input as a regression test after the fix. Keep the broader property too: the saved example protects the discovered case, while generation keeps searching the domain.
For numerical oracles, tolerances and the difference between an invariant and an absolute benchmark, read Testing Numerical Code. The public page is a preview; the full lesson requires membership.
Sources
- Hypothesis quickstart, domain and distribution, and glossary: checked documentation labelled version 6.170.12; library generation and shrinking behaviour.
- Python 3.14 random: local generators, inclusive integer bounds and reproducibility limitations.
- Python 3.14 assert statement: assertion execution and removal under optimisation.