pbt-hypothesis
Skill1 starsChanged 30 days agoMITView on GitHub
Hypothesis-specific patterns for property-based testing in Python — strategies, RuleBasedStateMachine, settings, and Hypothesis ecosystem gotchas. Use this skill whenever the task involves Hypothesis tests, the `hypothesis` package, `@given`, `st.` strategies, `RuleBasedStateMachine`, `@example`, or any Python property-based testing work. This skill pairs with the core `property-based-testing` skill, which handles property discovery and design — load both together for any Hypothesis task. This skill does not re-derive the workflow; it assumes the core skill is loaded and only covers Hypothesis syntax, idioms, and library-specific patterns.
In this repositoryOne other file it gives its agents.
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 10
If you have not done the discovery work yet, stop and do it first.
- Very specific: line 175
Always layer @example(...) decorators on @given for known-tricky inputs:
- Very specific: line 203
When Hypothesis reports a failure, it prints a @reproduce_failure(...) decorator.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Property-Based Testing with Hypothesis (Python)
This skill covers Hypothesis-specific patterns. It assumes the core property-based-testing skill is loaded and you have already worked through the discovery workflow (understand the function → brainstorm properties → name oracles → reject traps). This file picks up at Step 5 of that workflow: turning a chosen property into a Hypothesis test.
If you have not done the discovery work yet, stop and do it first. Hypothesis syntax is easy; finding the right property is hard, and skipping that step produces the tautologies and weak tests this skill exists to prevent.
Reference files
references/strategies.md— Strategy patterns: composite, recursive, FK-aware, shrinking-friendly designsreferences/stateful.md—RuleBasedStateMachine, rules, invariants, preconditions, bundlesreferences/settings.md— Profiles, deadlines, example database, CI tuning
Quickstart by category
A minimal Hypothesis test:
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sort_permutation(xs):
"""sort returns a permutation of its input.
Oracle: collections.Counter (independent of my_sort).
"""
from collections import Counter
assert Counter(my_sort(xs)) == Counter(xs)Key elements every Hypothesis test should have:
@given(...)with a strategy that matches the input domain precisely- A docstring stating the property in English, naming the oracle, and naming the trigger region the strategy must reach
- An assertion that fails meaningfully — not just
assert result - Where applicable,
@example(...)decorators pinning known-tricky cases
Strategies: the 30-second tour
For full coverage see references/strategies.md. The essentials:
| Need | Strategy |
| Integer in a range | st.integers(min_value=0, max_value=100) |
| Finite float | st.floats(allow_nan=False, allow_infinity=False) |
| ASCII text | st.text(alphabet=string.printable) |
| List with constraints | st.lists(elem, min_size=1, unique=True) |
| Dict | st.dictionaries(key_strategy, value_strategy) |
| Dataclass / class | st.builds(MyClass, field1=strategy1, ...) |
| Custom structure | @st.composite (see below) |
| One of several | st.one_of(strat1, strat2, ...) |
| Recursive (trees, JSON) | st.recursive(base, lambda c: extend(c), max_leaves=N) |
| From a regex | st.from_regex(r"...", fullmatch=True) |
@st.composite — the workhorse
For any input with internal structure or cross-field constraints:
@st.composite
def valid_dates(draw):
year = draw(st.integers(min_value=1900, max_value=2100))
month = draw(st.integers(min_value=1, max_value=12))
max_day = calendar.monthrange(year, month)[1]
day = draw(st.integers(min_value=1, max_value=max_day))
return datetime(year, month, day)
@given(valid_dates())
def test_date_arithmetic(date):
...This is dramatically better than st.dates().filter(lambda d: ...) — composite generators shrink well and don't waste budget.
FK-aware generation (relevant for SqlProof-style work)
Generate parent rows first, then draw children from the parent keys:
@st.composite
def order_with_customer(draw):
customers = draw(st.lists(
st.fixed_dictionaries({
"id": st.integers(min_value=1, max_value=1000),
"name": st.text(min_size=1, max_size=50),
}),
unique_by=lambda c: c["id"],
min_size=1, max_size=20,
))
customer_ids = [c["id"] for c in customers]
orders = draw(st.lists(
st.fixed_dictionaries({
"id": st.integers(min_value=1),
"customer_id": st.sampled_from(customer_ids),
"amount": st.decimals(min_value=0, max_value=10000, places=2),
}),
max_size=50,
))
return {"customers": customers, "orders": orders}The st.sampled_from(parent_keys) step guarantees referential integrity without any filtering.
Stateful testing: RuleBasedStateMachine
For systems with state (databases, caches, classes with mutable instances), flat @given won't find sequence-dependent bugs. Use RuleBasedStateMachine. Quick sketch:
from hypothesis.stateful import RuleBasedStateMachine, rule, invariant, precondition
class StackMachine(RuleBasedStateMachine):
def __init__(self):
super().__init__()
self.stack = MyStack()
self.model = [] # reference model
@rule(x=st.integers())
def push(self, x):
self.stack.push(x)
self.model.append(x)
@precondition(lambda self: self.model)
@rule()
def pop(self):
assert self.stack.pop() == self.model.pop()
@invariant()
def size_matches(self):
assert self.stack.size() == len(self.model)
TestStack = StackMachine.TestCaseFor full coverage of Bundle, consumes(), multi-actor patterns, and lifecycle handling: read references/stateful.md.
Hypothesis-specific gotchas
These are mistakes that are syntactically valid Hypothesis but produce bad tests. The general PBT anti-patterns are in the core skill's references/anti-patterns.md; the items below are Hypothesis-specific traps not covered there.
Don't mutate strategy outputs in place
# BAD
@given(st.lists(st.integers()))
def test_mutating(xs):
xs.sort() # mutates the input Hypothesis gave you
# ...Hypothesis may reuse strategy outputs across runs (especially during shrinking). Mutating them produces nondeterministic test failures. Always copy:
@given(st.lists(st.integers()))
def test_no_mutation(xs):
xs_copy = list(xs)
xs_copy.sort()
# ...assume() is not free
Each assume(cond) that fails costs Hypothesis an example. If your assumption fails often, you get warned with HealthCheck.filter_too_much — but the warning is the symptom, not the cause. The cause is a strategy that doesn't match the input domain. Fix the strategy, don't suppress the health check.
The quieter cost is worse: an assume that rejects only a few inputs raises no health check, and may be rejecting exactly the inputs that break the property — assume(a != b) in a dedupe test, assume(src != dst) in a transfer test. Before keeping any assume, check it only removes inputs outside the contract (core skill, Step 5.5 and anti-pattern #14).
st.floats() defaults include NaN and infinity
assert a + b == b + a fails for NaN because NaN is not equal to itself. Always use st.floats(allow_nan=False, allow_infinity=False) unless you're explicitly testing NaN behavior.
@example should pin known edge cases
Always layer @example(...) decorators on @given for known-tricky inputs:
@given(st.lists(st.integers()))
@example(xs=[])
@example(xs=[0])
@example(xs=[1, 1, 1])
def test_dedupe(xs):
...These run in addition to the random search, guaranteeing those cases are always tested.
Deadline issues with slow tests
Hypothesis's default 200ms per-example deadline trips often on tests that hit a database, filesystem, or network. Either disable it for those tests or set a generous deadline:
from hypothesis import settings
from datetime import timedelta
@settings(deadline=None) # or deadline=timedelta(seconds=5)
@given(...)
def test_slow_thing(...): ...Reproduce failures cleanly
When Hypothesis reports a failure, it prints a @reproduce_failure(...) decorator. Paste it temporarily onto the test to re-run the same counterexample without searching. Don't try to manually reproduce — use the blob.
Commit the example database
Hypothesis records failures to .hypothesis/examples. Commit this directory (or share it via CI cache) so found bugs reproduce immediately on every run, building a free regression suite over time.
Workflow integration
Recall the core workflow:
- Understand the function under test
- Brainstorm ≥5 candidate properties
- Name the oracle for each
- Reject the obvious traps
- Design the strategy ← Hypothesis specifics start here
- Check it reaches the trigger region (core Step 5.5) ← ranges, alphabets, size caps,
assume/.filter,@examplefor narrow triggers
- Check it reaches the trigger region (core Step 5.5) ← ranges, alphabets, size caps,
- Write the test, then critique it ← and continue here
- Use stateful testing when appropriate ←
RuleBasedStateMachineif so
When the property is identified and the oracle is clear, the Hypothesis-specific work is: pick the right strategy (often @st.composite), write the @given and assertion with a docstring stating the property, pin known edge cases with @example, and tune settings if needed. The library is small once you know what you're testing.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

