pydantic

High-performance Python data validation with type hints, Rust-powered core, and seamless FastAPI/Django integration. Validates data at runtime using Python type hints with automatic type coercion; strict mode available per-field or model-wide Supports nested models, recursive types, generics, and custom validators (field-level and model-level) for complex validation logic Includes built-in types for emails, URLs, file paths, secrets, and constrained integers/strings; extensible via custom Pydantic core schemas Serializes to dict, JSON, or custom formats with field-level control; integrates directly with FastAPI request/response models and SQLAlchemy ORM Settings management via BaseSettings for environment variables and config files; computed fields for derived properties

INSTALLATION
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill pydantic
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

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 Config` with `model_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_validator` with `@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')

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card