Cheatsheet for testing Pydantic models with pytest.
Basic happy path
def test_user_valid():
user = User.model_validate({"id": 1, "email": "[email protected]", "name": "Alice"})
assert user.id == 1
assert user.email == "[email protected]"
Parametrized validation
import pytest
from pydantic import ValidationError
@pytest.mark.parametrize("payload, ok", [
({"id": 1, "email": "[email protected]"}, True),
({"id": "abc", "email": "[email protected]"}, False),
({"email": "[email protected]"}, False),
({"id": 1, "email": "not-email"}, False),
])
def test_user_validation(payload, ok):
if ok:
User.model_validate(payload)
else:
with pytest.raises(ValidationError):
User.model_validate(payload)
Assert specific error
def test_age_negative():
with pytest.raises(ValidationError) as exc:
User.model_validate({"id": 1, "email": "[email protected]", "age": -1})
errs = exc.value.errors()
assert any(e["loc"] == ("age",) and "non-negative" in e["msg"] for e in errs)
Match error type
def test_email_format():
with pytest.raises(ValidationError) as exc:
User.model_validate({"id": 1, "email": "not-an-email"})
errs = exc.value.errors()
assert any(e["type"] == "value_error" for e in errs)
Round-trip
def test_roundtrip(user_factory):
user = user_factory()
data = user.model_dump(mode="json")
restored = User.model_validate(data)
assert restored == user
Factory (test fixtures)
import pytest
import factory
class UserFactory(factory.Factory):
class Meta:
model = User
id = factory.Sequence(lambda n: n + 1)
email = factory.Sequence(lambda n: f"user{n}@example.com")
name = factory.Faker("name")
@pytest.fixture
def user_factory():
return UserFactory
def test_with_factory(user_factory):
user = user_factory()
assert user.id > 0
polyfactory (Pydantic-aware)
from polyfactory.factories.pydantic_factory import ModelFactory
class UserFactory(ModelFactory[User]):
__model__ = User
def test_random():
user = UserFactory.build()
assert isinstance(user, User)
Auto-generates random valid instances.
Property-based testing (Hypothesis)
from hypothesis import given, strategies as st
@given(st.integers(min_value=1), st.emails())
def test_user_property(id, email):
user = User.model_validate({"id": id, "email": email})
assert user.id == id
Catches edge cases automatically.
Custom error messages
def test_password_complexity():
with pytest.raises(ValidationError, match="must contain uppercase"):
Password.model_validate("alllowercase")
match regex against the string repr.
Schema snapshot
def test_schema_snapshot(snapshot):
schema = User.model_json_schema()
snapshot.assert_match(json.dumps(schema, indent=2), "user_schema.json")
Catches accidental API breakage. Using syrupy or similar snapshot lib.
Equality
def test_user_equality():
a = User(id=1, email="[email protected]")
b = User(id=1, email="[email protected]")
assert a == b # value equality
Mutability
def test_frozen():
class Frozen(BaseModel):
model_config = {"frozen": True}
x: int
f = Frozen(x=1)
with pytest.raises(ValidationError):
f.x = 2
Test custom validator
def test_custom_validator():
@field_validator("email", mode="before")
@classmethod
def lower(cls, v):
return v.lower() if isinstance(v, str) else v
user = User.model_validate({"id": 1, "email": "[email protected]"})
assert user.email == "[email protected]"
Test computed_field
def test_full_name():
user = User(first="Alice", last="X")
assert user.full_name == "Alice X"
assert "full_name" in user.model_dump()
Common mistakes
- Asserting on internal
e.value.__cause__— not portable; usee.value.errors(). - Forgetting
pytest.raisesfor failure case — test silently passes. - Testing only happy path — edge cases ship.
- Factory boy without Pydantic awareness — generated data fails Pydantic.
Read this next
If you want my polyfactory + Hypothesis Pydantic test patterns, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .