hypothesis-strategies
Custom Hypothesis strategy patterns for property-based testing. Activated when designing test data generators or property tests.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Hypothesis strategies
Purpose
Guide for designing custom Hypothesis strategies for property-based testing. Covers strategy composition, custom generators, and shrinking behavior.
When to use
This skill activates when:
- Creating custom data generators
- Composing complex test inputs
- Designing property-based tests
- Debugging shrinking behavior
- Generating domain-specific data
Core concepts
Basic strategies
from hypothesis import strategies as st
# Basic types
text = st.text()
integers = st.integers()
floats = st.floats()
booleans = st.booleans()
# Constrained types
positive_ints = st.integers(min_value=1)
short_text = st.text(max_size=100)
ascii_text = st.text(alphabet=string.ascii_letters)
Strategy composition
from hypothesis import strategies as st
# Combine strategies
point = st.tuples(st.floats(), st.floats())
person = st.fixed_dictionaries({
'name': st.text(min_size=1),
'age': st.integers(min_value=0, max_value=150),
})
# One of multiple options
value = st.one_of(st.text(), st.integers(), st.booleans())
# Optional values
maybe_text = st.text() | st.none()
Custom strategies
Using @composite
from hypothesis import strategies as st
from hypothesis.strategies import composite, DrawFn
@composite
def valid_identifiers(draw: DrawFn) -> str:
"""Generate valid Python identifiers."""
first = draw(st.sampled_from(string.ascii_letters + '_'))
rest = draw(st.text(
alphabet=string.ascii_letters + string.digits + '_',
max_size=30
))
return first + rest
@composite
def valid_emails(draw: DrawFn) -> str:
"""Generate valid email addresses."""
local = draw(st.text(
alphabet=string.ascii_lowercase + string.digits + '._',
min_size=1,
max_size=64,
))
domain = draw(st.text(
alphabet=string.ascii_lowercase,
min_size=1,
max_size=20,
))
tld = draw(st.sampled_from(['com', 'org', 'net', 'io']))
return f"{local}@{domain}.{tld}"
Building from existing strategies
@composite
def config_dicts(draw: DrawFn) -> dict:
"""Generate valid configuration dictionaries."""
name = draw(valid_identifiers())
options = draw(st.lists(st.text(), max_size=10))
enabled = draw(st.booleans())
timeout = draw(st.integers(min_value=1, max_value=3600) | st.none())
config = {
'name': name,
'options': options,
'enabled': enabled,
}
if timeout is not None:
config['timeout'] = timeout
return config
Recursive strategies
@composite
def nested_dicts(draw: DrawFn, max_depth: int = 3) -> dict:
"""Generate nested dictionary structures."""
if max_depth <= 0:
# Base case: simple values only
return draw(st.fixed_dictionaries({
'value': st.one_of(st.text(), st.integers(), st.booleans())
}))
# Recursive case
return draw(st.fixed_dictionaries({
'value': st.one_of(st.text(), st.integers(), st.booleans()),
'children': st.lists(
st.deferred(lambda: nested_dicts(max_depth=max_depth - 1)),
max_size=3
)
}))
Strategy for domain types
Dataclass strategies
from dataclasses import dataclass
from hypothesis import strategies as st
@dataclass
class User:
id: int
name: str
email: str
active: bool
@composite
def users(draw: DrawFn) -> User:
"""Generate valid User instances."""
return User(
id=draw(st.integers(min_value=1)),
name=draw(st.text(min_size=1, max_size=100)),
email=draw(valid_emails()),
active=draw(st.booleans()),
)
Enum strategies
from enum import Enum
class Status(Enum):
PENDING = 'pending'
ACTIVE = 'active'
COMPLETED = 'completed'
# Generate any status
status_strategy = st.sampled_from(Status)
# Generate only certain statuses
active_statuses = st.sampled_from([Status.PENDING, Status.ACTIVE])
Using with @given
Basic usage
from hypothesis import given
@given(users())
def test_user_has_valid_id(user: User):
"""User IDs are always positive."""
assert user.id > 0
@given(st.lists(users(), min_size=1))
def test_user_list_not_empty(users: list[User]):
"""Generated user lists have at least one user."""
assert len(users) >= 1
With assume
from hypothesis import given, assume
@given(st.integers(), st.integers())
def test_division(a: int, b: int):
"""Test division with valid divisor."""
assume(b != 0)
result = a / b
assert result * b == a
Multiple strategies
@given(
user=users(),
permissions=st.lists(st.sampled_from(['read', 'write', 'admin'])),
)
def test_user_with_permissions(user: User, permissions: list[str]):
"""Test user with various permission sets."""
result = apply_permissions(user, permissions)
assert all(p in result.permissions for p in permissions)
Controlling shrinking
@composite
def ids_with_checksum(draw: DrawFn) -> str:
"""Generate IDs where parts must stay together during shrinking."""
# Use map to preserve relationships during shrinking
parts = draw(st.lists(st.integers(min_value=0, max_value=9), min_size=4, max_size=4))
checksum = sum(parts) % 10
return ''.join(map(str, parts)) + str(checksum)
Debugging strategies
# See what a strategy generates
from hypothesis import settings, Verbosity
@settings(verbosity=Verbosity.verbose)
@given(users())
def test_debug_user_generation(user: User):
"""See generated values."""
pass # Values printed to output
# Generate examples without running tests
from hypothesis import find
# Find example that satisfies predicate
example = find(users(), lambda u: u.active and len(u.name) > 5)
Checklist
- Strategy generates valid domain data
- Edge cases covered (empty, max size, special values)
- Constraints properly enforced
- Shrinking produces meaningful minimal examples
- Strategy is composable with others
Additional resources:
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!