ProvenPin Install

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 13:32 UTC
Status
passed
Last passed
2026-10-15 13:32 UTC
Tests
3 / 3
Language
Python
Runtime
Python 3.13.16
pinned versions3
hypothesis@6.169.4pydantic@2.14.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38742087302

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.

requirements.txt
hypothesis==6.169.4
pydantic==2.14.1
pytest==9.1.1
models.py
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
inventory.py
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())
tests/test_models.py
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.ini
[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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.