Pydantic
debt(d7/e5/b5/t5)
Closest to 'only careful code review or runtime testing' (d7). Missing Pydantic validation at API boundaries isn't flagged by mypy/ruff — they check static types, not whether runtime validation exists. Manual isinstance() checks pass linting fine; the absence of Pydantic shows up only in code review or when malformed input hits production.
Closest to 'touches multiple files / significant refactor in one component' (e5). Per quick_fix, introducing Pydantic means defining BaseModel classes and replacing manual validation across API handlers — not a one-liner. Migrating V1→V2 patterns (@validator → @field_validator, dict() → model_dump()) compounds the effort within the component.
Closest to 'persistent productivity tax' (b5). Pydantic applies across web and CLI contexts (per applies_to) and becomes load-bearing at every data boundary; every new endpoint or settings module is shaped by model definitions. Not architectural (b7+) since it's swappable per-module, but a persistent presence.
Closest to 'notable trap most devs eventually learn' (t5). Per misconception, devs think Pydantic is FastAPI-only. Per common_mistakes, the default coercion ('42' → 42 without strict=True) and post-construction mutation bypassing validators are documented gotchas that contradict the 'validation always protects me' intuition.
Also Known As
TL;DR
Explanation
Pydantic V2 (Rust-powered) validates data against type-annotated models. Fields support validators, default values, aliases, and computed properties. BaseModel parses and validates on instantiation — invalid data raises ValidationError with detailed field-level errors. Pydantic is the foundation of FastAPI and is widely used for settings management (BaseSettings), API request/response models, and data pipeline validation.
Common Misconception
Why It Matters
Common Mistakes
- Using Pydantic V1 patterns in V2 — @validator is replaced by @field_validator, class Config by model_config.
- Not using model.model_dump() (V2) vs dict() (V1) — API has changed between versions.
- Expecting Pydantic to validate output, not just input — validation happens at parse time; mutating fields after construction bypasses validators.
- Not using model_config = ConfigDict(strict=True) when coercion should be forbidden — Pydantic coerces '42' to int by default.
Code Examples
# Manual validation — verbose and easy to miss:
def create_user(data: dict) -> dict:
if not isinstance(data.get('email'), str) or '@' not in data['email']:
raise ValueError('Invalid email')
if not isinstance(data.get('age'), int) or data['age'] < 0:
raise ValueError('Invalid age')
return {'email': data['email'], 'age': data['age']}
from pydantic import BaseModel, EmailStr, Field
class CreateUserRequest(BaseModel):
email: EmailStr
age: int = Field(ge=0, le=150)
name: str = Field(min_length=1, max_length=100)
# Usage:
try:
user = CreateUserRequest(**request_data) # Validates and coerces
except ValidationError as e:
print(e.errors()) # Detailed field-level errors