Technical
Pydantic Models: The Best Python Feature You Are Not Using Enough
I used to validate data with if-statements scattered across my codebase. Missing a check meant a production bug. Now Pydantic models handle all validation in one place, and bugs from bad data have dropped to near zero.
What Pydantic Does
Pydantic turns Python type hints into runtime validation. You define a model with type annotations, and Pydantic validates every value, converts types when possible, and raises clear errors when validation fails.
from pydantic import BaseModel, Field, EmailStr
from typing import Optional, Literal
class SubscriberCreate(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=100)
source: Optional[str] = None
status: Literal['active', 'unsubscribed'] = 'active'That model does more validation than 20 lines of if-statements. Email format, string length, allowed values, optional fields with defaults. All declared, all automatic.
Why Pydantic Beats Manual Validation
Manual validation has three problems:
- It is scattered: Validation logic lives in route handlers, service functions, database layers. Finding all the checks for one field requires searching the entire codebase.
- It is inconsistent: Different endpoints validate the same field differently because each developer wrote their own checks.
- It is incomplete: Manual checks inevitably miss edge cases. What about empty strings? Whitespace-only strings? Unicode characters? Negative numbers?
Pydantic centralizes all validation in the model definition. One source of truth.
Five Patterns I Use Daily
Pattern 1: Separate Create and Response Models
class PostCreate(BaseModel):
title: str
body: str
class PostResponse(BaseModel):
slug: str
title: str
body: str
createdAt: str
readingTime: intThe client sends PostCreate (minimal fields). The API returns PostResponse (computed fields included). Different shapes for different directions.
Pattern 2: Field Constraints
class Article(BaseModel):
title: str = Field(min_length=5, max_length=200)
word_count: int = Field(ge=100, le=5000)Pattern 3: Enum Validation
status: Literal['draft', 'scheduled', 'published']
# Rejects 'DRAFT', 'active', '', or any other valuePattern 4: Default Factories
categories: list[str] = Field(default_factory=list)
# Each instance gets its own empty list (no shared mutable default)Pattern 5: Computed Fields
Use validators or model_post_init to compute values from other fields, like generating a slug from a title or calculating reading time from body length.
The FastAPI Integration
FastAPI uses Pydantic models directly as request and response types. When a request does not match the model, FastAPI returns a 422 error with details about exactly which field failed and why. No manual error handling code needed.
If you are writing Python APIs and not using Pydantic, you are doing too much work.
See the Pydantic documentation for the complete field types and validator reference.
RELATED READING
The Consulting Shift I Am Making In Year Two
After a year of writing and building, my consulting practice is changing shape. Shorter engagements. Sharper outcomes.
ReadThe Frontend Shift: Shipping Less JavaScript In Year Two
A year ago I reached for Next.js for everything. This year I often reach for nothing.
ReadThe Serverless Lesson I Would Write On A Sticky Note
After a year of shipping serverless projects, one rule explains most of the wins and all of the losses.
Read