Claude Code Plugins

Community-maintained marketplace

Feedback

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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 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')

[Full integration patterns available in respective skills if deployed together]

Additional Resources