Hypothesis property tests for Pydantic v2 models: working versions (tested 2026-10-15)
Property-based tests with Hypothesis on Pydantic v2 models: JSON round trips, validator invariants, and a stateful RuleBasedStateMachine for a small inventory class.
passed3 passed · 0 failed2026-10-15 11:56 UTC- Status
- passed
- Last passed
- 2026-10-15 11:56 UTC
- Tests
- 3 / 3
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("pytest-hypothesis-pydantic-roundtrip") (install), or see them on GitHub.
hypothesis==6.169.4 pydantic==2.14.1 pytest==9.1.1
from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field, field_validator
class Category(StrEnum):
TOOL = "tool"
FOOD = "food"
class Item(BaseModel):
model_config = ConfigDict(frozen=True)
sku: str = Field(min_length=1, max_length=12)
qty: int = Field(ge=0)
price: float = Field(ge=0, allow_inf_nan=False)
category: Category
tags: tuple[str, ...] = ()
@field_validator("sku")
@classmethod
def normalize_sku(cls, v: str) -> str:
# Strip and uppercase. Whitespace-only input must fail, not become "".
v = v.strip().upper()
if not v:
raise ValueError("sku must not be blank")
# max_length runs BEFORE this validator, and upper() can grow a string
# ("\u00df" -> "SS"), so re-check the length after normalizing.
if len(v) > 12:
raise ValueError("sku too long after normalization")
return v
from collections import Counter
class Inventory:
def __init__(self) -> None:
self._qty: Counter[str] = Counter()
def add(self, sku: str, qty: int) -> None:
if qty <= 0:
raise ValueError("qty must be positive")
self._qty[sku] += qty
def remove(self, sku: str, qty: int) -> None:
if qty <= 0:
raise ValueError("qty must be positive")
if self._qty[sku] < qty:
raise ValueError(f"only {self._qty[sku]} of {sku} in stock")
self._qty[sku] -= qty
if self._qty[sku] == 0:
del self._qty[sku]
def count(self, sku: str) -> int:
return self._qty[sku]
def total(self) -> int:
return sum(self._qty.values())
from hypothesis import given, strategies as st
from pydantic import ValidationError
from models import Category, Item
def items():
# st.builds calls Item(...), so every generated value must pass the validators.
# Generate sku with surrounding spaces to exercise normalization.
# allow_nan=False and allow_infinity=False: NaN/inf are not valid JSON numbers.
return st.builds(
Item,
sku=st.from_regex(r" ?[a-z0-9]{1,10} ?", fullmatch=True),
qty=st.integers(min_value=0, max_value=10**6),
price=st.floats(min_value=0, allow_nan=False, allow_infinity=False),
category=st.from_type(Category),
tags=st.lists(st.text(max_size=8), max_size=3).map(tuple),
)
@given(items())
def test_json_roundtrip(item):
assert Item.model_validate_json(item.model_dump_json()) == item
@given(items())
def test_python_dump_roundtrip(item):
assert Item.model_validate(item.model_dump()) == item
@given(st.text(max_size=20))
def test_sku_validator_is_idempotent(raw):
# Arbitrary text is either rejected or normalized. Accepted values must be stable.
try:
item = Item(sku=raw, qty=0, price=0.0, category=Category.TOOL)
except ValidationError:
return
assert Item(**item.model_dump()).sku == item.sku
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
FastAPI + Pydantic v2 validators, discriminated unions and serializers
Pydantic v2 patterns in FastAPI that agents often get wrong: field_validator, model_validator, discriminated unions, computed_field and response_model filtering.
FastAPI background tasks + dependency overrides
FastAPI BackgroundTasks for post-response work, with dependency_overrides to swap services in tests.
FastAPI endpoint with Pydantic v2 discriminated unions and Annotated validators
Accept a tagged union of payment methods with Field(discriminator), add Annotated validators, and test the exact 422 error shapes offline with TestClient.
httpx async API client + respx mocks
A typed async API client on httpx with retries on 5xx and Pydantic parsing, tested offline with respx route mocks.