| name | pydantic |
| description | Python data validation using type hints and runtime type checking with Pydantic v2's Rust-powered core for high-performance validation in FastAPI, Django, and configuration management. |
| progressive_disclosure | [object Object] |
| token_estimates | [object Object] |
Pydantic Validation Skill
Summary
Python data validation using type hints and runtime type checking with Pydantic v2's Rust-powered core for high-performance validation.
When to Use
- API request/response validation (FastAPI, Django)
- Settings and configuration management (env variables, config files)
- ORM model validation (SQLAlchemy integration)
- Data parsing and serialization (JSON, dict, custom formats)
- Type-safe data classes with automatic validation
- CLI argument parsing with type safety
Quick Start
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
class User(BaseModel):
id: int
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
created_at: datetime = Field(default_factory=datetime.now)
is_active: bool = True
# Validate data
user = User(id=1, name="Alice", email="alice@example.com")
print(user.model_dump()) # {'id': 1, 'name': 'Alice', ...}
# Automatic type coercion
user2 = User(id="2", name="Bob", email="bob@example.com")
assert user2.id == 2 # String "2" coerced to int
# Validation error
try:
User(id=3, name="", email="invalid")
except ValidationError as e:
print(e.errors())
Core Concepts
BaseModel Foundation
from pydantic import BaseModel, ConfigDict
class Product(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
use_enum_values=True,
arbitrary_types_allowed=False
)
name: str
price: float
quantity: int = 0
# Usage
product = Product(name=" Widget ", price=19.99)
assert product.name == "Widget" # Whitespace stripped
# Validate on assignment
product.price = "29.99" # Auto-converts to float
Field Configuration
from pydantic import Field, field_validator
from typing import Annotated
class Item(BaseModel):
# Field constraints
sku: str = Field(pattern=r'^[A-Z]{3}-\d{4}$')
price: float = Field(gt=0, le=10000)
stock: int = Field(ge=0, default=0)
# Annotated types (Pydantic v2)
quantity: Annotated[int, Field(ge=1, le=100)]
# Descriptions and examples
description: str = Field(
...,
description="Product description",
examples=["High-quality widget"]
)
# Deprecated fields
old_field: str | None = Field(None, deprecated=True)
@field_validator('sku')
@classmethod
def validate_sku(cls, v: str) -> str:
if not v.startswith('ABC'):
raise ValueError('SKU must start with ABC')
return v
Pydantic v2 Improvements
Migration from v1
# Pydantic v1
class OldModel(BaseModel):
class Config:
validate_assignment = True
json_encoders = {datetime: lambda v: v.isoformat()}
# Pydantic v2
class NewModel(BaseModel):
model_config = ConfigDict(
validate_assignment=True,
# json_encoders replaced by serializers
)
@model_serializer
def ser_model(self) -> dict:
return {...}
# Key changes:
# - .dict() → .model_dump()
# - .json() → .model_dump_json()
# - .parse_obj() → .model_validate()
# - .parse_raw() → .model_validate_json()
# - @validator → @field_validator
# - @root_validator → @model_validator
Performance Improvements
# v2 uses Rust core (pydantic-core) for 5-50x speedup
from pydantic import BaseModel
import time
class Data(BaseModel):
values: list[int]
names: list[str]
# Benchmark
data = {'values': list(range(10000)), 'names': ['item'] * 10000}
start = time.perf_counter()
for _ in range(1000):
Data.model_validate(data)
elapsed = time.perf_counter() - start
print(f"Validated 1000 iterations in {elapsed:.2f}s")
Field Types
Built-in Types
from pydantic import (
BaseModel, EmailStr, HttpUrl, UUID4,
FilePath, DirectoryPath, Json, SecretStr,
PositiveInt, NegativeFloat, conint, constr
)
from typing import Literal
from pathlib import Path
class Example(BaseModel):
# Email validation
email: EmailStr
# URL validation
website: HttpUrl
# UUID
id: UUID4
# File system paths
config_file: FilePath
data_dir: DirectoryPath
# JSON string → parsed object
metadata: Json[dict[str, str]]
# Secret (won't print in logs)
api_key: SecretStr
# Constrained types
age: PositiveInt
balance: NegativeFloat
username: constr(min_length=3, max_length=20, pattern=r'^[a-z]+$')
code: conint(ge=1000, le=9999)
# Literal types
status: Literal['pending', 'approved', 'rejected']
Custom Types
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic_core import core_schema
from typing import Any
class Color:
def __init__(self, r: int, g: int, b: int):
self.r, self.g, self.b = r, g, b
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.no_info_after_validator_function(
cls.validate,
core_schema.str_schema()
)
@classmethod
def validate(cls, v: str) -> 'Color':
if not v.startswith('#') or len(v) != 7:
raise ValueError('Invalid hex color')
r = int(v[1:3], 16)
g = int(v[3:5], 16)
b = int(v[5:7], 16)
return cls(r, g, b)
class Design(BaseModel):
primary_color: Color
# Usage
design = Design(primary_color='#FF5733')
assert design.primary_color.r == 255
Validators
Field Validators
from pydantic import field_validator, model_validator
class Account(BaseModel):
username: str
password: str
password_confirm: str
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError('must be alphanumeric')
return v
@field_validator('password')
@classmethod
def password_strong(cls, v: str) -> str:
if len(v) < 8:
raise ValueError('must be at least 8 characters')
if not any(c.isupper() for c in v):
raise ValueError('must contain uppercase letter')
return v
# Validate multiple fields
@field_validator('username', 'password')
@classmethod
def not_empty(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError('must not be empty')
return v.strip()
Model Validators
from pydantic import model_validator
from typing import Self
class DateRange(BaseModel):
start_date: datetime
end_date: datetime
@model_validator(mode='after')
def check_dates(self) -> Self:
if self.end_date < self.start_date:
raise ValueError('end_date must be after start_date')
return self
class Order(BaseModel):
items: list[str]
total: float
discount: float = 0
@model_validator(mode='before')
@classmethod
def calculate_total(cls, data: dict) -> dict:
# Pre-processing before validation
if isinstance(data, dict) and 'total' not in data:
data['total'] = len(data.get('items', [])) * 10.0
return data
Root Validators (Wrap)
from pydantic import model_validator, ValidationInfo
class Config(BaseModel):
env: Literal['dev', 'prod']
debug: bool = False
@model_validator(mode='wrap')
@classmethod
def validate_config(cls, values: Any, handler, info: ValidationInfo):
# Call default validation
result = handler(values)
# Post-validation logic
if result.env == 'prod' and result.debug:
raise ValueError('debug cannot be True in production')
return result
Type Coercion and Strict Mode
from pydantic import BaseModel, ConfigDict, ValidationError
# Coercive mode (default)
class CoerciveModel(BaseModel):
count: int
price: float
data = CoerciveModel(count="42", price="19.99")
assert data.count == 42 # String → int
assert data.price == 19.99 # String → float
# Strict mode
class StrictModel(BaseModel):
model_config = ConfigDict(strict=True)
count: int
price: float
try:
StrictModel(count="42", price="19.99") # Raises ValidationError
except ValidationError as e:
print("Strict mode: no coercion allowed")
# Per-field strict mode
class MixedModel(BaseModel):
flexible: int # Allows coercion
strict: Annotated[int, Field(strict=True)] # No coercion
MixedModel(flexible="1", strict=2) # OK
# MixedModel(flexible="1", strict="2") # ValidationError
Nested Models and Recursive Types
from pydantic import BaseModel
from typing import ForwardRef
# Nested models
class Address(BaseModel):
street: str
city: str
country: str
class Company(BaseModel):
name: str
address: Address
company = Company(
name="ACME Corp",
address={'street': '123 Main St', 'city': 'NYC', 'country': 'USA'}
)
# Recursive types (tree structure)
class TreeNode(BaseModel):
value: int
children: list['TreeNode'] = []
TreeNode.model_rebuild() # Required for forward references
tree = TreeNode(
value=1,
children=[
TreeNode(value=2, children=[TreeNode(value=4)]),
TreeNode(value=3)
]
)
# Self-referencing with ForwardRef
class Category(BaseModel):
name: str
parent: 'Category | None' = None
subcategories: list['Category'] = []
Category.model_rebuild()
Generic Models
from pydantic import BaseModel
from typing import Generic, TypeVar
T = TypeVar('T')
class Response(BaseModel, Generic[T]):
success: bool
data: T
message: str = ''
class User(BaseModel):
id: int
name: str
# Usage with concrete type
user_response = Response[User](
success=True,
data=User(id=1, name='Alice')
)
# List response
list_response = Response[list[User]](
success=True,
data=[User(id=1, name='Alice'), User(id=2, name='Bob')]
)
# Generic repository pattern
class Repository(BaseModel, Generic[T]):
items: list[T]
def add(self, item: T) -> None:
self.items.append(item)
user_repo = Repository[User](items=[])
user_repo.add(User(id=1, name='Alice'))
Serialization
Model Dump
from pydantic import BaseModel, Field, field_serializer
class Article(BaseModel):
title: str
content: str
tags: list[str]
metadata: dict[str, Any] = {}
# Serialization customization
@field_serializer('tags')
def serialize_tags(self, tags: list[str]) -> str:
return ','.join(tags)
article = Article(
title='Pydantic Guide',
content='...',
tags=['python', 'validation']
)
# Dump to dict
data = article.model_dump()
# {'title': 'Pydantic Guide', 'tags': 'python,validation', ...}
# Exclude fields
data = article.model_dump(exclude={'metadata'})
# Include only specific fields
data = article.model_dump(include={'title', 'tags'})
# Exclude unset fields
article2 = Article(title='Test', content='...', tags=[])
data = article2.model_dump(exclude_unset=True) # metadata excluded
# By alias
class AliasModel(BaseModel):
internal_name: str = Field(alias='externalName')
model = AliasModel(externalName='value')
model.model_dump(by_alias=True) # {'externalName': 'value'}
JSON Serialization
from datetime import datetime
from pydantic import BaseModel, field_serializer
class Event(BaseModel):
name: str
timestamp: datetime
@field_serializer('timestamp')
def serialize_dt(self, dt: datetime) -> str:
return dt.isoformat()
event = Event(name='Deploy', timestamp=datetime.now())
# Dump to JSON string
json_str = event.model_dump_json()
# '{"name":"Deploy","timestamp":"2025-11-30T..."}'
# Pretty print
json_str = event.model_dump_json(indent=2)
# Parse from JSON
event2 = Event.model_validate_json(json_str)
Custom Serializers
from pydantic import model_serializer
class User(BaseModel):
id: int
username: str
password: SecretStr
@model_serializer
def ser_model(self) -> dict[str, Any]:
return {
'id': self.id,
'username': self.username,
# Never serialize password
}
user = User(id=1, username='alice', password='secret123')
assert 'password' not in user.model_dump()
Settings Management
BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(
env_file='.env',
env_file_encoding='utf-8',
env_prefix='APP_',
case_sensitive=False
)
# Environment variables
database_url: str
redis_url: str = 'redis://localhost:6379'
secret_key: SecretStr
debug: bool = False
# Nested settings
class SMTPSettings(BaseModel):
host: str
port: int = 587
username: str
password: SecretStr
smtp: SMTPSettings
# Reads from environment variables:
# APP_DATABASE_URL, APP_REDIS_URL, APP_SECRET_KEY, APP_DEBUG
# APP_SMTP__HOST, APP_SMTP__PORT, etc.
settings = AppSettings()
Multi-Environment Settings
from functools import lru_cache
class Settings(BaseSettings):
environment: Literal['dev', 'staging', 'prod'] = 'dev'
database_url: str
api_key: SecretStr
model_config = SettingsConfigDict(
env_file='.env',
extra='ignore'
)
@property
def is_production(self) -> bool:
return self.environment == 'prod'
@lru_cache
def get_settings() -> Settings:
return Settings()
# Usage in FastAPI
from fastapi import Depends
@app.get('/config')
def get_config(settings: Settings = Depends(get_settings)):
return {'env': settings.environment}
FastAPI Integration
Request/Response Models
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=50)
email: EmailStr
password: str = Field(min_length=8)
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
model_config = ConfigDict(from_attributes=True)
@app.post('/users', response_model=UserResponse)
def create_user(user: UserCreate):
# FastAPI auto-validates request body
# Returns only fields in UserResponse (password excluded)
return UserResponse(
id=1,
username=user.username,
email=user.email
)
Query Parameters
from pydantic import BaseModel, Field
from fastapi import Query
class PaginationParams(BaseModel):
skip: int = Field(0, ge=0)
limit: int = Field(10, ge=1, le=100)
class SearchParams(BaseModel):
q: str = Field(..., min_length=1)
category: str | None = None
sort_by: Literal['date', 'relevance'] = 'relevance'
@app.get('/search')
def search(params: SearchParams = Query()):
return {'query': params.q, 'sort': params.sort_by}
Response Model Customization
class DetailedUser(BaseModel):
id: int
username: str
email: EmailStr
created_at: datetime
last_login: datetime | None
@app.get('/users/{user_id}', response_model=DetailedUser)
def get_user(user_id: int, include_dates: bool = False):
user = DetailedUser(
id=user_id,
username='alice',
email='alice@example.com',
created_at=datetime.now(),
last_login=None
)
if not include_dates:
return user.model_dump(exclude={'created_at', 'last_login'})
return user
SQLAlchemy Integration
ORM Models with Pydantic
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import DeclarativeBase
from pydantic import BaseModel, ConfigDict
class Base(DeclarativeBase):
pass
# SQLAlchemy ORM model
class UserDB(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True)
email = Column(String(100))
created_at = Column(DateTime, default=datetime.utcnow)
# Pydantic model for validation
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
email: EmailStr
created_at: datetime
# Usage
from sqlalchemy.orm import Session
def get_user(db: Session, user_id: int) -> UserSchema:
user = db.query(UserDB).filter(UserDB.id == user_id).first()
return UserSchema.model_validate(user) # ORM → Pydantic
Hybrid Approach
from pydantic import BaseModel
class UserBase(BaseModel):
username: str
email: EmailStr
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
username: str | None = None
email: EmailStr | None = None
password: str | None = None
class UserInDB(UserBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
password_hash: str
# CRUD operations
def create_user(db: Session, user: UserCreate) -> UserInDB:
db_user = UserDB(
username=user.username,
email=user.email,
password_hash=hash_password(user.password)
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return UserInDB.model_validate(db_user)
Django Integration
Django Model Validation
from django.db import models
from pydantic import BaseModel, field_validator
# Django model
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
published = models.BooleanField(default=False)
# Pydantic schema
class ArticleSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
title: str = Field(max_length=200)
content: str
published: bool = False
@field_validator('content')
@classmethod
def validate_content(cls, v: str) -> str:
if len(v) < 100:
raise ValueError('Content too short')
return v
# Usage in Django views
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(['POST'])
def create_article(request):
try:
data = ArticleSchema.model_validate_json(request.body)
article = Article.objects.create(**data.model_dump())
return JsonResponse({'id': article.id})
except ValidationError as e:
return JsonResponse({'errors': e.errors()}, status=400)
Computed Fields
from pydantic import computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
return self.width * self.height
@computed_field
@property
def perimeter(self) -> float:
return 2 * (self.width + self.height)
rect = Rectangle(width=10, height=5)
assert rect.area == 50
assert rect.perimeter == 30
# Computed fields in serialization
data = rect.model_dump()
# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}
Custom Errors
from pydantic import BaseModel, field_validator, ValidationError
from pydantic_core import PydanticCustomError
class StrictUser(BaseModel):
username: str
age: int
@field_validator('username')
@classmethod
def validate_username(cls, v: str) -> str:
if len(v) < 3:
raise PydanticCustomError(
'username_too_short',
'Username must be at least 3 characters',
{'min_length': 3, 'actual_length': len(v)}
)
return v
@field_validator('age')
@classmethod
def validate_age(cls, v: int) -> int:
if v < 18:
raise PydanticCustomError(
'underage',
'User must be at least 18 years old',
{'age': v, 'minimum_age': 18}
)
return v
# Custom error handling
try:
StrictUser(username='ab', age=16)
except ValidationError as e:
for error in e.errors():
print(f"{error['type']}: {error['msg']}")
print(f"Context: {error.get('ctx')}")
Performance Optimization
V2 Rust Core Benefits
# Pydantic v2 uses pydantic-core (Rust) for:
# - 5-50x faster validation
# - Lower memory usage
# - Better error messages
# - Improved JSON parsing
import timeit
from pydantic import BaseModel
class Data(BaseModel):
values: list[int]
names: list[str]
metadata: dict[str, Any]
# Benchmark
data_dict = {
'values': list(range(1000)),
'names': ['item'] * 1000,
'metadata': {'key': 'value'}
}
def validate():
Data.model_validate(data_dict)
time_taken = timeit.timeit(validate, number=10000)
print(f"10000 validations: {time_taken:.2f}s")
Optimization Techniques
from pydantic import BaseModel, ConfigDict
class OptimizedModel(BaseModel):
model_config = ConfigDict(
# Validate assignment only when needed
validate_assignment=False,
# Disable validation for internal use
validate_default=False,
# Use slots for memory efficiency
# (Not available in Pydantic v2 BaseModel directly)
)
data: list[int]
# Reuse validators
from functools import lru_cache
@lru_cache(maxsize=128)
def get_validator(model_class):
return model_class.model_validate
# Bulk validation
def validate_bulk(items: list[dict]) -> list[Data]:
validator = get_validator(Data)
return [validator(item) for item in items]
JSON Schema Generation
from pydantic import BaseModel, Field
class Product(BaseModel):
"""Product model for catalog"""
id: int = Field(description="Unique product identifier")
name: str = Field(description="Product name", examples=["Widget"])
price: float = Field(gt=0, description="Price in USD")
tags: list[str] = Field(default=[], description="Product tags")
# Generate JSON Schema
schema = Product.model_json_schema()
print(json.dumps(schema, indent=2))
# {
# "title": "Product",
# "description": "Product model for catalog",
# "type": "object",
# "properties": {
# "id": {"type": "integer", "description": "Unique product identifier"},
# "name": {"type": "string", "description": "Product name"},
# ...
# },
# "required": ["id", "name", "price"]
# }
# OpenAPI compatible
from fastapi import FastAPI
app = FastAPI()
@app.post('/products')
def create_product(product: Product):
return product
# FastAPI auto-generates OpenAPI schema from Pydantic models
Dataclass Integration
from pydantic.dataclasses import dataclass
from pydantic import Field
@dataclass
class User:
id: int
name: str = Field(min_length=1)
email: str = Field(pattern=r'.+@.+\..+')
# Works like Pydantic BaseModel with validation
user = User(id=1, name='Alice', email='alice@example.com')
# Validation on construction
try:
User(id=2, name='', email='invalid')
except ValidationError as e:
print(e.errors())
# Convert to Pydantic BaseModel
from pydantic import BaseModel
class UserModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
email: str
user_model = UserModel.model_validate(user)
Testing Strategies
Unit Testing Models
import pytest
from pydantic import ValidationError
def test_user_validation():
# Valid data
user = User(id=1, name='Alice', email='alice@example.com')
assert user.name == 'Alice'
# Invalid data
with pytest.raises(ValidationError) as exc_info:
User(id='invalid', name='Bob', email='bob@example.com')
errors = exc_info.value.errors()
assert errors[0]['type'] == 'int_parsing'
def test_user_serialization():
user = User(id=1, name='Alice', email='alice@example.com')
data = user.model_dump()
assert data == {
'id': 1,
'name': 'Alice',
'email': 'alice@example.com'
}
def test_nested_validation():
company = Company(
name='ACME',
address={'street': '123 Main', 'city': 'NYC', 'country': 'USA'}
)
assert company.address.city == 'NYC'
Testing with Fixtures
@pytest.fixture
def sample_user_data():
return {
'id': 1,
'name': 'Alice',
'email': 'alice@example.com'
}
@pytest.fixture
def sample_user(sample_user_data):
return User(**sample_user_data)
def test_with_fixtures(sample_user):
assert sample_user.name == 'Alice'
def test_invalid_email(sample_user_data):
sample_user_data['email'] = 'invalid'
with pytest.raises(ValidationError):
User(**sample_user_data)
Property-Based Testing
from hypothesis import given, strategies as st
@given(
id=st.integers(min_value=1),
name=st.text(min_size=1, max_size=100),
email=st.emails()
)
def test_user_always_valid(id, name, email):
user = User(id=id, name=name, email=email)
assert user.id == id
assert user.name == name
assert user.email == email
Migration Guide (v1 → v2)
Key Changes
# v1
from pydantic import BaseModel
class OldModel(BaseModel):
class Config:
validate_assignment = True
arbitrary_types_allowed = True
# Validators
@validator('field')
def validate_field(cls, v):
return v
@root_validator
def validate_model(cls, values):
return values
# Serialization
data = model.dict()
json_str = model.json()
# Parsing
model = OldModel.parse_obj(data)
model = OldModel.parse_raw(json_str)
# v2
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
class NewModel(BaseModel):
model_config = ConfigDict(
validate_assignment=True,
arbitrary_types_allowed=True
)
# Field validators
@field_validator('field')
@classmethod
def validate_field(cls, v):
return v
# Model validators
@model_validator(mode='after')
def validate_model(self):
return self
# Serialization
data = model.model_dump()
json_str = model.model_dump_json()
# Parsing
model = NewModel.model_validate(data)
model = NewModel.model_validate_json(json_str)
Migration Checklist
- Replace
class Configwithmodel_config = ConfigDict() - Update
.dict()→.model_dump() - Update
.json()→.model_dump_json() - Update
.parse_obj()→.model_validate() - Update
.parse_raw()→.model_validate_json() - Update
@validator→@field_validatorwith@classmethod - Update
@root_validator→@model_validator(mode='after') - Review
json_encoders→ use@field_serializer - Test strict mode behavior changes
- Update custom types to use
__get_pydantic_core_schema__
Best Practices
Model Organization
# Separate schemas by use case
class UserBase(BaseModel):
"""Shared fields"""
username: str
email: EmailStr
class UserCreate(UserBase):
"""API request for creating user"""
password: str
class UserUpdate(BaseModel):
"""API request for updating user (all optional)"""
username: str | None = None
email: EmailStr | None = None
password: str | None = None
class UserInDB(UserBase):
"""Database representation"""
model_config = ConfigDict(from_attributes=True)
id: int
password_hash: str
created_at: datetime
class UserResponse(UserBase):
"""API response (excludes sensitive data)"""
id: int
created_at: datetime
Validation Best Practices
# Use Field for constraints, not validators
class Good(BaseModel):
age: int = Field(ge=0, le=150)
email: EmailStr
class Bad(BaseModel):
age: int
email: str
@field_validator('age')
@classmethod
def validate_age(cls, v):
if v < 0 or v > 150:
raise ValueError('invalid age')
return v
# Prefer composition over inheritance
class TimestampMixin(BaseModel):
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class User(TimestampMixin):
username: str
email: EmailStr
Error Handling
from pydantic import ValidationError
def safe_validate(data: dict) -> User | None:
try:
return User.model_validate(data)
except ValidationError as e:
# Log validation errors
logger.error(f"Validation failed: {e.errors()}")
return None
def validate_with_details(data: dict):
try:
return User.model_validate(data)
except ValidationError as e:
# Return user-friendly errors
return {
'success': False,
'errors': [
{
'field': '.'.join(str(loc) for loc in err['loc']),
'message': err['msg'],
'type': err['type']
}
for err in e.errors()
]
}
Common Patterns
API Response Wrapper
from typing import Generic, TypeVar
T = TypeVar('T')
class APIResponse(BaseModel, Generic[T]):
success: bool
data: T | None = None
error: str | None = None
metadata: dict[str, Any] = {}
# Usage
user_response = APIResponse[User](
success=True,
data=User(id=1, name='Alice', email='alice@example.com')
)
error_response = APIResponse[User](
success=False,
error='User not found'
)
Pagination
class PaginatedResponse(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
page_size: int
@computed_field
@property
def total_pages(self) -> int:
return (self.total + self.page_size - 1) // self.page_size
users = PaginatedResponse[User](
items=[...],
total=100,
page=1,
page_size=10
)
assert users.total_pages == 10
Audit Fields
class AuditMixin(BaseModel):
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
created_by: int | None = None
updated_by: int | None = None
class Document(AuditMixin):
title: str
content: str
@model_validator(mode='before')
@classmethod
def update_timestamp(cls, data: dict) -> dict:
if isinstance(data, dict):
data['updated_at'] = datetime.utcnow()
return data
Related Skills
When using Pydantic, consider these complementary skills:
- fastapi-local-dev: FastAPI development server patterns with Pydantic integration
- sqlalchemy: SQLAlchemy ORM patterns for database models with Pydantic validation
- django: Django framework integration with Pydantic schemas
- pytest: Testing strategies for Pydantic models and validation
Quick FastAPI Integration Reference (Inlined for Standalone Use)
# FastAPI with Pydantic (basic pattern)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
model_config = ConfigDict(from_attributes=True)
@app.post('/users', response_model=UserResponse)
def create_user(user: UserCreate):
# FastAPI auto-validates using Pydantic
# response_model filters out password
return UserResponse(id=1, username=user.username, email=user.email)
Quick SQLAlchemy Integration Reference (Inlined for Standalone Use)
# SQLAlchemy 2.0 with Pydantic validation
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase
from pydantic import BaseModel, ConfigDict
class Base(DeclarativeBase):
pass
class UserDB(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50))
email = Column(String(100))
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
email: str
# Convert ORM to Pydantic
user_orm = db.query(UserDB).first()
user_validated = UserSchema.model_validate(user_orm)
Quick Pytest Testing Reference (Inlined for Standalone Use)
# Testing Pydantic models with pytest
import pytest
from pydantic import ValidationError
def test_user_validation():
user = User(id=1, name='Alice', email='alice@example.com')
assert user.name == 'Alice'
def test_validation_error():
with pytest.raises(ValidationError) as exc_info:
User(id='invalid', name='Bob', email='bob@example.com')
errors = exc_info.value.errors()
assert errors[0]['type'] == 'int_parsing'
@pytest.fixture
def sample_user():
return User(id=1, name='Alice', email='alice@example.com')
[Full integration patterns available in respective skills if deployed together]