Chapter 10, the final chapter: Pydantic in production. Performance, FastAPI integration patterns, and alternatives when the hot path demands more.
Performance characteristics
Pydantic v2 is fast. Validating typical models: tens of thousands per second. Serializing: similar.
For most apps: not the bottleneck. DB / HTTP / network dominate.
Where it can matter
- Massive batch ingestion: 1M records / minute through validation.
- Hot RPC with strict latency budget.
- Tight loops validating many small messages.
For these: profile first, optimize second.
Caching schemas
Pydantic compiles a schema per class. Subsequent uses are fast.
For dynamically generated models:
def make_model(fields: dict):
return create_model("Dynamic", **fields)
create_model rebuilds; cache the result if you call repeatedly.
TypeAdapter at module scope
USER_LIST = TypeAdapter(list[User])
def parse(data):
return USER_LIST.validate_python(data)
Don’t recreate per call. Schema compilation isn’t free.
model_validate_json over model_validate(dict)
# Faster
User.model_validate_json(raw_bytes)
# Slower
User.model_validate(json.loads(raw_bytes))
Pydantic’s JSON path is Rust end-to-end; skips Python’s json decode.
Frozen for immutable
class User(BaseModel):
model_config = {"frozen": True}
Hashable; no copy on use as dict key. Microbenefit but real for hot paths.
defer_build
For many models, defer compilation until first use:
class M(BaseModel):
model_config = {"defer_build": True}
Saves startup time. Fields validated once at first use.
FastAPI patterns
Separate Read / Create / Update models
class UserBase(BaseModel):
email: EmailStr
name: str
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
email: EmailStr | None = None
name: str | None = None
class UserRead(UserBase):
id: int
created_at: datetime
Clear contract per operation.
from_attributes for ORM
class UserRead(BaseModel):
id: int
email: str
model_config = {"from_attributes": True}
Lets FastAPI return SQLAlchemy instances; Pydantic converts.
@app.get("/users/{id}", response_model=UserRead)
async def get_user(id: int, db = Depends(get_db)):
return await db.get(User, id) # SQLAlchemy User → UserRead
Response model exclude
@app.get("/users/{id}", response_model=UserOut, response_model_exclude={"email"})
Per-request shape control.
Validation context
For db-dependent validation in FastAPI:
@app.post("/users")
async def create(user: UserCreate, db: AsyncSession = Depends(get_db)):
if await user_exists(db, user.email):
raise HTTPException(409, "email taken")
...
Don’t put DB-dependent validation in Pydantic validators (they should be pure). Validate at the handler layer.
Streaming validation
For large lists:
adapter = TypeAdapter(list[User])
# Lazy if you can
def parse_stream(records):
for r in records:
yield User.model_validate(r)
Don’t accumulate; stream.
Custom JSON encoder for non-Pydantic types
class M(BaseModel):
custom: MyType
@field_serializer("custom", when_used="json")
def serialize_custom(self, v: MyType, _info):
return str(v)
Don’t override json.dumps globally; use field_serializer or PlainSerializer.
Alternatives
msgspec
5-10× faster than Pydantic for narrow validation:
import msgspec
class User(msgspec.Struct):
id: int
email: str
user = msgspec.json.decode(raw_bytes, type=User)
- C-implemented; very fast.
- Smaller feature set.
- No validators, computed_field, custom serializers, JSON Schema generation, settings.
Use when: high-throughput message decoding, latency-critical, willing to give up Pydantic features.
attrs
from attrs import define
@define
class User:
id: int
email: str
Pure dataclass replacement; some validation. No validation by default; not for API I/O.
Plain dataclasses
Stdlib. No validation. For internal types.
TypedDict
For dict-shaped data with type hints. Not a validator.
Decision tree
| Need | Pick |
|---|---|
| API I/O (FastAPI) | Pydantic |
| LLM structured output | Pydantic |
| Settings / config | pydantic-settings |
| JSON Schema export | Pydantic |
| 1M msg/sec decode | msgspec |
| Internal value types | attrs / dataclasses |
| MIxed | Pydantic at boundaries; msgspec/attrs internally |
For most apps: Pydantic is the right answer at the boundary.
What you’ve learned
This textbook covered:
- The mental model.
- Fields, types, constraints.
- Validators (field, model, before, after).
- Serialization.
- Nested, generics, discriminated unions.
- Custom types, TypeAdapter.
- Strict mode and coercion.
- JSON Schema.
- Settings.
- Performance and alternatives.
Pair with:
If you want my full Pydantic + FastAPI + SQLAlchemy starter, 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 .