Pydantic Patterns
Type-safe data validation with Pydantic v2
PRINCIPLES
- •Schema-First: Design models before implementation
- •Validation Built-in: Leverage automatic type coercion and validation
- •Serialization Control: Customize JSON encoding/decoding
- •Settings Management: Use pydantic-settings for configuration
- •Performance: Use model_config for optimization
MODEL BASICS
Basic Model Definition
python
from pydantic import BaseModel, Field, ConfigDict
from datetime import datetime
from typing import Annotated
class User(BaseModel):
"""User model with validation."""
model_config = ConfigDict(
str_strip_whitespace=True, # Strip whitespace from strings
validate_assignment=True, # Validate on attribute assignment
extra="forbid", # Forbid extra fields
)
id: int
username: str = Field(..., min_length=3, max_length=50)
email: str = Field(..., pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
full_name: str | None = None
is_active: bool = True
created_at: datetime = Field(default_factory=datetime.utcnow)
FIELD VALIDATION
Field Constraints
python
from pydantic import Field, PositiveInt, constr, conint
class Product(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0, description="Price in USD")
quantity: PositiveInt = Field(default=1)
sku: constr(pattern=r"^[A-Z]{3}-\d{6}$") = Field(...)
rating: conint(ge=1, le=5) | None = None
# Using Annotated for reusable constraints
from typing import Annotated
PositiveFloat = Annotated[float, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]
class Order(BaseModel):
total: PositiveFloat
customer_name: NonEmptyStr
CUSTOM VALIDATORS
Field and Model Validators
python
from pydantic import BaseModel, field_validator, model_validator
import re
class UserCreate(BaseModel):
username: str
password: str
password_confirm: str
@field_validator("username")
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not re.match(r"^[a-zA-Z0-9_]+$", v):
raise ValueError("Username must be alphanumeric")
return v.lower()
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if not any(c.isupper() for c in v):
raise ValueError("Password must contain uppercase letter")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain a digit")
return v
@model_validator(mode="after")
def passwords_match(self) -> "UserCreate":
if self.password != self.password_confirm:
raise ValueError("Passwords do not match")
return self
# Before validator for transformation
class NormalizedInput(BaseModel):
tags: list[str]
@field_validator("tags", mode="before")
@classmethod
def split_tags(cls, v):
if isinstance(v, str):
return [t.strip().lower() for t in v.split(",")]
return v
NESTED MODELS
Composition Patterns
python
from pydantic import BaseModel
from datetime import datetime
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class Company(BaseModel):
name: str
address: Address
class Employee(BaseModel):
id: int
name: str
company: Company
addresses: list[Address] = []
# Self-referencing models
from typing import ForwardRef
class TreeNode(BaseModel):
value: str
children: list["TreeNode"] = []
TreeNode.model_rebuild() # Required for forward references
SERIALIZATION
Model Export Control
python
from pydantic import BaseModel, Field, ConfigDict
from datetime import datetime
class UserInDB(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
hashed_password: str = Field(exclude=True) # Never serialize
created_at: datetime
# Computed fields
@property
def email_domain(self) -> str:
return self.email.split("@")[1]
# Serialization options
user = UserInDB(id=1, email="test@example.com", hashed_password="xxx", created_at=datetime.now())
# Exclude fields
user.model_dump(exclude={"hashed_password"})
# Include only specific fields
user.model_dump(include={"id", "email"})
# JSON serialization with custom encoder
user.model_dump_json(indent=2)
# From ORM objects
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class UserModel(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String)
# Works with from_attributes=True
user_schema = UserInDB.model_validate(user_model)
SETTINGS MANAGEMENT
Environment Configuration
python
from pydantic import Field, PostgresDsn, RedisDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# App settings
app_name: str = "MyAPI"
debug: bool = False
# Database
database_url: PostgresDsn
db_pool_size: int = Field(default=5, ge=1, le=100)
# Redis
redis_url: RedisDsn | None = None
# Security
secret_key: str = Field(..., min_length=32)
access_token_expire_minutes: int = 30
# Nested settings using env prefix
class Config:
env_nested_delimiter = "__"
# Usage
settings = Settings()
# .env file:
# DATABASE_URL=postgresql://user:pass@localhost/db
# SECRET_KEY=your-32-character-secret-key-here
# DEBUG=true
GENERIC MODELS
Reusable Patterns
python
from pydantic import BaseModel
from typing import Generic, TypeVar
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
page_size: int
pages: int
class APIResponse(BaseModel, Generic[T]):
success: bool = True
data: T | None = None
error: str | None = None
# Usage
class User(BaseModel):
id: int
name: str
response: PaginatedResponse[User] = PaginatedResponse(
items=[User(id=1, name="Alice")],
total=1,
page=1,
page_size=10,
pages=1,
)
ANTI-PATTERNS
❌ AVOID:
python
# Mutable default values
class Bad(BaseModel):
items: list = [] # Shared between instances!
# No validation
class NoValidation(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
data: any # Loses type safety
✅ PREFER:
python
# Factory defaults
class Good(BaseModel):
items: list[str] = Field(default_factory=list)
# Strict typing
class StrictModel(BaseModel):
data: dict[str, int] # Full type information