Dataclasses, Pydantic and Data Validation
Model data cleanly in Python: dataclasses for internal containers with invariants, Pydantic for parsing and validating untrusted input, and how both use type hints, then build a mini validator yourself.
🎯 What you'll learn
- Dataclass features: frozen, ordering, default_factory, __post_init__
- Pydantic models, constraints and validators
- Choosing between dataclasses, Pydantic and TypedDict
- How libraries use type hints at runtime
📚 Before you start
1Dataclasses
@dataclass generates __init__, __repr__ and __eq__ (and optionally ordering, hashing and immutability) from annotated fields:
from dataclasses import dataclass, field
from typing import List
@dataclass(frozen=True, order=True)
class Money:
amount: int # cents
currency: str = "EUR"
@dataclass
class Order:
id: int
lines: List[str] = field(default_factory=list) # never a mutable default directly
def __post_init__(self):
if self.id <= 0:
raise ValueError("id must be positive")frozen=Truemakes instances immutable (and hashable);order=Trueadds comparisons by field order.slots=True(3.10+) andkw_only=True(3.10+) save memory and force keyword arguments.dataclasses.asdict,replace(obj, amount=5)(a copy with changes).- Dataclasses don't validate types at runtime:
Money(amount="ten")is accepted unless you check in__post_init__.
2Pydantic
Pydantic models parse and validate untrusted data at runtime from type hints:
from pydantic import BaseModel, Field, EmailStr, field_validator
class CreateUser(BaseModel):
email: EmailStr
age: int = Field(ge=13, le=120)
tags: list[str] = []
@field_validator("tags")
@classmethod
def lowercase(cls, v): return [t.lower() for t in v]
user = CreateUser.model_validate({"email": "ana@x.io", "age": "30"}) # "30" coerced to 30
CreateUser.model_validate({"email": "bad", "age": 5}) # ValidationError listing both fields
user.model_dump_json() # serializationPydantic v2 has a Rust core (fast), strict and lax modes, settings management (pydantic-settings reads env vars), and JSON Schema generation, which FastAPI uses for request validation and OpenAPI docs.
3When to Use What
| Need | Choose |
|---|---|
| Internal data containers, no untrusted input | @dataclass (or NamedTuple for tiny records) |
| Validating external input: API bodies, config, messages | Pydantic models |
| Typed dicts for JSON you don't control | TypedDict (static checking only) |
| Domain objects with behaviour and invariants | Classes or dataclasses with validation in constructors |
Validate at the boundary (HTTP handler, message consumer, config loader), then pass well-typed objects inward.
4Type Hints Drive Everything
Both tools read annotations. typing.get_type_hints(cls) resolves them; typing.get_origin / get_args decompose generics like List[int] or Optional[str]. That's how libraries know to coerce "30" into an int or to validate each list element.
⚠️ Common Pitfalls
- Mutable defaults in dataclass fields: use
field(default_factory=list). - Assuming dataclasses validate types.
- Validating deep inside business logic instead of at the boundary.
- Lax coercion surprises (
"1"→Truefor bools); use strict mode where it matters. - Pydantic v1 vs v2 API differences (
dict()→model_dump(),parse_obj→model_validate).
📋 Quick Reference
@dataclass: __init__/__repr__/__eq__ · frozen · order · field(default_factory=…) · __post_init__ for checks
Pydantic: parse + validate + coerce from type hints · Field constraints · validators · model_dump(_json)
dataclass internally, Pydantic at the boundary · TypedDict for static shapes · get_type_hints / get_origin / get_args🧠 Key takeaways
- Dataclasses generate boilerplate; use default_factory and __post_init__ for invariants.
- Pydantic parses, coerces and validates external data from type hints.
- Validate at the boundary, pass typed objects inward.
get_type_hints,get_originandget_argspower runtime type handling.
🎤 Interview Q&A
Q1Dataclass vs Pydantic model?core▾
Dataclasses generate __init__/__repr__/__eq__ for internal data without runtime validation. Pydantic models parse, coerce and validate untrusted data with detailed errors, serialization and JSON Schema. Use Pydantic at boundaries, dataclasses internally.
- Pydantic dataclasses?
pydantic.dataclasses.dataclassadds validation to dataclass syntax.
Q2How do you enforce invariants in a dataclass?core▾
In __post_init__, raise on invalid values; make it frozen=True so invariants can't be broken later; use replace() for modified copies.
- Computed fields?
field(init=False)set in __post_init__.
Q3What changed in Pydantic v2?core▾
A Rust core (much faster), model_validate/model_dump instead of parse_obj/dict, field_validator/model_validator decorators, strict mode, and pydantic-settings split out.
- Migration tool? bump-pydantic.
Q4How does Pydantic know how to validate a field?senior▾
It reads type hints (resolving forward references), builds a validation schema per field (with constraints from Field and Annotated metadata), and generates efficient validators in pydantic-core.
- Annotated?
Annotated[int, Field(gt=0)]attaches constraints to types.
Q5When would you use TypedDict?core▾
To describe the shape of dicts (e.g. JSON you pass around) for static type checkers without runtime cost or conversion; it doesn't validate at runtime.
- NotRequired? Marks optional keys (3.11+ or typing_extensions).
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.