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

NeedPick
API I/O (FastAPI)Pydantic
LLM structured outputPydantic
Settings / configpydantic-settings
JSON Schema exportPydantic
1M msg/sec decodemsgspec
Internal value typesattrs / dataclasses
MIxedPydantic at boundaries; msgspec/attrs internally

For most apps: Pydantic is the right answer at the boundary.

What you’ve learned

This textbook covered:

  1. The mental model.
  2. Fields, types, constraints.
  3. Validators (field, model, before, after).
  4. Serialization.
  5. Nested, generics, discriminated unions.
  6. Custom types, TypeAdapter.
  7. Strict mode and coercion.
  8. JSON Schema.
  9. Settings.
  10. 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 .