Agent Skills: Pydantic

Data validation using Python type hints with Pydantic models, settings, serialization, and performance optimization

UncategorizedID: CodeAtCode/oss-ai-skills/pydantic

Install this agent skill to your local

pnpm dlx add-skill https://github.com/CodeAtCode/oss-ai-skills/tree/HEAD/frameworks/pydantic

Skill Files

Browse the full folder contents for pydantic.

Download Skill

Loading file tree…

frameworks/pydantic/SKILL.md

Skill Metadata

Name
pydantic
Description
"Data validation using Python type hints with Pydantic models, settings, serialization, and performance optimization"

Pydantic

Data validation using Python type annotations.

Overview

Pydantic is a Python library that provides data validation and settings management using Python type annotations. It validates data at runtime and generates JSON Schema for automatic documentation.

Key Features:

  • Data validation using type hints
  • Automatic data coercion
  • JSON Schema generation
  • Serialization/deserialization
  • Settings management with environment variables
  • Fast performance (especially v2)
  • Extensive customization options

Installation

# Basic installation
pip install pydantic

# With email validation
pip install pydantic[email]

# With best performance (recommended)
pip install pydantic[email] orjson

# Version 2 (current)
pip install pydantic>=2.0.0

Basic Models

Creating Models

from pydantic import BaseModel, Field
from typing import Optional, List

class User(BaseModel):
    """Basic user model."""
    id: int
    name: str
    email: str
    age: Optional[int] = None
    is_active: bool = True

# Create instance
user = User(id=1, name="John", email="john@example.com")
print(user)
# id=1 name='John' email='john@example.com' age=None is_active=True

# From dictionary
data = {
    "id": 2,
    "name": "Jane",
    "email": "jane@example.com",
    "age": 25
}
user = User(**data)

# From JSON
json_data = '{"id": 3, "name": "Bob", "email": "bob@example.com"}'
user = User.model_validate_json(json_data)

Field Types

from pydantic import BaseModel
from typing import Optional, List, Dict, Set, Tuple, Union, Any

class AllTypesExample(BaseModel):
    # Primitives
    string: str
    integer: int
    floating: float
    boolean: bool
    bytes: bytes
    
    # Optional
    optional_string: Optional[str] = None
    optional_with_default: Optional[int] = 42
    
    # Collections
    list_of_strings: List[str] = []
    list_of_ints: List[int]
    dict_data: Dict[str, int] = {}
    set_of_ints: Set[int] = set()
    tuple_data: Tuple[int, str, float] = (1, "a", 1.5)
    
    # Union
    union_field: Union[int, str] = 1
    
    # Any
    any_field: Any = "anything"
    
    # Literal
    from typing import Literal
    status: Literal["pending", "active", "completed"] = "pending"

Nested Models

from pydantic import BaseModel, Field
from typing import List, Optional

class ChildModel(BaseModel):
    """Nested child model with alias for input."""
    child_id: int
    child_name: str

class ParentModel(BaseModel):
    """Parent containing child model with Field alias."""
    parent_id: int
    parent_name: str
    child_data: ChildModel = Field(alias='child_data')  # Field alias for input
    
    def __init__(self, parent_id: int, parent_name: str, child_info: ChildModel = None, **data):
        super().__init__(
            parent_id=parent_id,
            parent_name=parent_name,
            child_data=child_info if child_info else data.get('child_data')
        )

class Person(BaseModel):
    name: str
    email: str
    address: Optional[Address] = None
    company: Optional[Company] = None

# Create nested data
person = Person(
    name="John",
    email="john@example.com",
    address=Address(
        street="123 Main St",
        city="New York",
        country="USA"
    ),
    company=Company(
        name="Tech Corp",
        address=Address(
            street="456 Business Ave",
            city="San Francisco",
            country="USA"
        ),
        employees=["Alice", "Bob"]
    )
)

Field Validation

Field Constraints

from pydantic import BaseModel, Field, field_validator
from typing import Optional
import re

class ConstrainedUser(BaseModel):
    # String constraints
    name: str = Field(min_length=1, max_length=100)
    username: str = Field(pattern=r'^[a-zA-Z0-9_]+$')
    email: str = Field(format="email")
    
    # Number constraints
    age: int = Field(ge=0, le=150)  # greater or equal, less or equal
    price: float = Field(gt=0)      # greater than
    quantity: int = Field(ge=0, default=0)
    
    # Collection constraints
    tags: List[str] = Field(min_length=1, max_length=10)
    scores: List[int] = Field(min_length=1, max_length=100)
    
    # Optional with constraint
    nickname: Optional[str] = Field(default=None, max_length=50)

# Validation examples
user = ConstrainedUser(
    name="John Doe",
    username="john_doe",
    email="john@example.com",
    age=25,
    price=19.99,
    tags=["python", "fastapi"]
)

Field Validators

from pydantic import BaseModel, field_validator, Field
import re

class ValidatedUser(BaseModel):
    username: str
    password: str
    age: int
    
    @field_validator('username')
    @classmethod
    def validate_username(cls, v: str) -> str:
        if len(v) < 3:
            raise ValueError('Username must be at least 3 characters')
        if not re.match(r'^[a-zA-Z0-9_]+$', v):
            raise ValueError('Username can only contain letters, numbers, and underscores')
        return v
    
    @field_validator('password')
    @classmethod
    def validate_password(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError('Password must be at least 8 characters')
        if not re.search(r'[A-Z]', v):
            raise ValueError('Password must contain at least one uppercase letter')
        if not re.search(r'[0-9]', v):
            raise ValueError('Password must contain at least one digit')
        return v
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 0 or v > 150:
            raise ValueError('Age must be between 0 and 150')
        return v

# Multiple validators on same field
class MultiValidatedField(BaseModel):
    value: str
    
    @field_validator('value')
    @classmethod
    def strip_value(cls, v: str) -> str:
        return v.strip()  # First: strip whitespace
    
    @field_validator('value')
    @classmethod
    def validate_not_empty(cls, v: str) -> str:
        if not v:
            raise ValueError('Value cannot be empty')
        return v  # Then: check not empty

Model Validators

from pydantic import BaseModel, model_validator, field_validator
from typing import Optional

class UserRegistration(BaseModel):
    password: str
    confirm_password: str
    username: str
    
    @model_validator(mode='before')
    @classmethod
    def check_passwords_match(cls, data):
        """Validate before any field validation."""
        if isinstance(data, dict):
            if data.get('password') != data.get('confirm_password'):
                raise ValueError('Passwords do not match')
        return data
    
    @model_validator(mode='after')
    def validate_username_not_password(self):
        """Validate after all field validation."""
        if self.username.lower() in self.password.lower():
            raise ValueError('Username cannot be part of the password')
        return self

# Root validator (alias for model_validator in v1)
class AdvancedValidation(BaseModel):
    start_date: str
    end_date: str
    
    @model_validator(mode='after')
    def validate_dates(self):
        from datetime import datetime
        start = datetime.fromisoformat(self.start_date)
        end = datetime.fromisoformat(self.end_date)
        
        if end < start:
            raise ValueError('End date must be after start date')
        
        return self

Serialization

Model Dump

from pydantic import BaseModel
from typing import List, Optional

class User(BaseModel):
    id: int
    name: str
    email: str
    tags: List[str] = []
    metadata: Optional[dict] = None

user = User(
    id=1,
    name="John",
    email="john@example.com",
    tags=["admin", "developer"],
    metadata={"department": "Engineering"}
)

# To dictionary
data = user.model_dump()
print(data)
# {'id': 1, 'name': 'John', 'email': 'john@example.com', 'tags': ['admin', 'developer'], 'metadata': {'department': 'Engineering'}}

# To JSON string
json_str = user.model_dump_json()
print(json_str)
# {"id":1,"name":"John",...}

# Include/Exclude fields
data = user.model_dump(include={'id', 'name'})  # Only id and name
data = user.model_dump(exclude={'metadata'})     # Everything except metadata

# Serialization Tips for clean API responses
# Exclude None values
data = user.model_dump(exclude_none=True)

# Exclude nested None values recursively
from pydantic import TypeAdapter
ta = TypeAdapter(List[Optional[str]])
clean_data = ta.dump_json(["a", None, "b"])  # Clean API responses

Advanced Serialization

from pydantic import BaseModel, Field, SerializerFunctionWrap
from typing import List, Optional
from datetime import datetime

class User(BaseModel):
    id: int
    name: str
    created_at: datetime
    
    # Custom serialization
    @property
    def display_name(self) -> str:
        return f"#{self.id} - {self.name}"
    
    # Custom field serializer
    @field_serializer('created_at')
    def serialize_datetime(self, dt: datetime) -> str:
        return dt.isoformat()

user = User(id=1, name="John", created_at=datetime.now())

# Serialization options
data = user.model_dump(
    mode='json',           # Convert to JSON-serializable types
    include={'id', 'name'},
    exclude={'created_at'},
    by_alias=True,         # Use field aliases
    exclude_none=True,    # Exclude None values
    exclude_unset=True,   # Exclude unset fields
    exclude_defaults=True # Exclude default values
)

# model_rebuild for dynamic schema generation
def dynamic_schema_gen():
    # Generate schema at runtime
    schema = {
        "type": "object",
        "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}
    }
    User.model_rebuild()  # Rebuild model with new schema
    return User

Model Validate

from pydantic import BaseModel
from typing import Optional

class User(BaseModel):
    id: int
    name: str
    email: str

# From dictionary
data = {"id": 1, "name": "John", "email": "john@example.com"}
user = User.model_validate(data)

# From JSON string
json_str = '{"id": 2, "name": "Jane", "email": "jane@example.com"}'
user = User.model_validate_json(json_str)

# From object
class SomeClass:
    def __init__(self):
        self.id = 3
        self.name = "Bob"
        self.email = "bob@example.com"

obj = SomeClass()
user = User.model_validate(obj)

# Partial update
data = {"name": "Updated Name"}
user = User.model_validate(data, update={"id": 1, "email": "old@example.com"})

JSON Schema

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str

# Generate JSON Schema
schema = User.model_json_schema()
print(schema)

# With configuration
class ConfiguredUser(BaseModel):
    model_config = {'title': 'User Model', 'description': 'A user model'}
    
    id: int
    name: str

schema = ConfiguredUser.model_json_schema()

Alias and Naming

Field Aliases

from pydantic import BaseModel, Field, AliasChoices

class UserWithAlias(BaseModel):
    # Use alias for input, different name for code
    user_id: int = Field(alias='userId')
    first_name: str = Field(alias='firstName')
    last_name: str = Field(alias='lastName')
    email_address: str = Field(validation_alias='email')  # AliasChoices for multiple

# Populate using alias
data = {"userId": 1, "firstName": "John", "lastName": "Doe", "email": "john@example.com"}
user = UserWithAlias.model_validate(data)

# Access by Python name
print(user.user_id)
print(user.first_name)

# Serialize with alias
json_data = user.model_dump(by_alias=True)
# {'userId': 1, 'firstName': 'John', 'lastName': 'Doe', 'email': 'john@example.com'}

Alias Generator

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel, to_snake, to_pascal

class CamelCaseUser(BaseModel):
    """User with camelCase serialization."""
    model_config = ConfigDict(alias_generator=to_camel)
    
    user_id: int
    first_name: str
    last_name: str
    email_address: str

user = CamelCaseUser(
    user_id=1,
    first_name="John",
    last_name="Doe",
    email_address="john@example.com"
)

# Serialized as camelCase
print(user.model_dump(by_alias=True))
# {'userId': 1, 'firstName': 'John', 'lastName': 'Doe', 'emailAddress': 'john@example.com'}

Default Values

Field Defaults

from pydantic import BaseModel, Field
from typing import Optional, List

class DefaultsExample(BaseModel):
    # Simple default
    name: str = "Unknown"
    
    # Default with type
    age: int = 25
    
    # Optional with default None
    nickname: Optional[str] = None
    
    # Required (no default)
    email: str
    
    # Field with default
    tags: List[str] = Field(default_factory=list)
    
    # Field with factory
    items: List[int] = Field(default_factory=lambda: [1, 2, 3])
    
    # Default factory for mutable objects (important!)
    metadata: dict = Field(default_factory=dict)
    scores: List[int] = Field(default_factory=list)

# Using default_factory for complex defaults
import uuid
class WithFactory(BaseModel):
    id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    created_at: str = Field(default_factory=lambda: "generated_value")

Default Values with Validators

from pydantic import BaseModel, field_validator, Field
from typing import Optional

class UserWithDefaultValidator(BaseModel):
    username: str
    # Validator runs even with default
    display_name: str = Field(default="")
    
    @field_validator('display_name', mode='before')
    @classmethod
    def use_username_as_display_name(cls, v, info):
        # If display_name not provided, use username
        if v == "":
            # info.data contains other field values
            return info.data.get('username', 'Unknown')
        return v

# Without display_name - uses username
user = UserWithDefaultValidator(username="john")
print(user.display_name)  # "john"

# With display_name - uses provided value
user = UserWithDefaultValidator(username="john", display_name="John Doe")
print(user.display_name)  # "John Doe"

Constrained Types

String Constraints

from pydantic import BaseModel, Field, constr

# Using Field
class FieldConstraints(BaseModel):
    username: str = Field(min_length=3, max_length=20)
    password: str = Field(min_length=8)
    slug: str = Field(pattern=r'^[a-z0-9-]+$')

# Using constrained types
class ConstrainedTypes(BaseModel):
    # constr creates a constrained string type
    username: constr(min_length=3, max_length=20)
    password: constr(min_length=8)
    slug: constr(pattern=r'^[a-z0-9-]+$')
    email: constr(pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')

user = ConstrainedTypes(
    username="john_doe",
    password="secret123",
    slug="my-blog-post",
    email="john@example.com"
)

Numeric Constraints

from pydantic import BaseModel, Field, conint, confloat, conlist

class NumericConstraints(BaseModel):
    # Integer constraints
    quantity: conint(ge=0, le=1000)
    age: conint(ge=0, le=150)
    
    # Float constraints
    price: confloat(gt=0)
    rating: confloat(ge=0.0, le=5.0)
    
    # List constraints
    scores: conlist(int, min_length=1, max_length=10)
    codes: conlist(str, min_length=3)

product = NumericConstraints(
    quantity=10,
    age=25,
    price=19.99,
    rating=4.5,
    scores=[90, 85, 95],
    codes=["ABC", "DEF"]
)

Special Types

Email, URL, UUID

from pydantic import BaseModel, EmailStr, HttpUrl, UUID1, UUID4, PaymentCardNumber
from typing import Optional
import uuid

class ContactInfo(BaseModel):
    # Email validation
    email: EmailStr
    secondary_email: Optional[EmailStr] = None
    
    # URL validation
    website: HttpUrl
    api_endpoint: Optional[HttpUrl] = None
    
    # UUID
    user_uuid: UUID1  # UUID version 1
    session_uuid: UUID4  # UUID version 4
    
    # Payment card (Luhn validation)
    card_number: Optional[PaymentCardNumber] = None

contact = ContactInfo(
    email="user@example.com",
    website="https://example.com",
    user_uuid=uuid.uuid1(),
    session_uuid=uuid.uuid4()
)

Secret Types

from pydantic import BaseModel, SecretStr, SecretBytes

class Credentials(BaseModel):
    # Masks value in output
    password: SecretStr
    api_key: Optional[SecretStr] = None
    
    # For binary secrets
    encryption_key: SecretBytes

creds = Credentials(password="secret123")

# Access the secret
print(creds.password)              # SecretStr('**********')
print(creds.password.get_secret_value())  # "secret123"

# Serialization
data = creds.model_dump()
# {'password': SecretStr('**********'), 'api_key': None, 'encryption_key': SecretBytes(b'**********')}

Enum Types

from pydantic import BaseModel, StrEnum, IntEnum
from enum import Enum

class Status(str, Enum):
    PENDING = "pending"
    ACTIVE = "active"
    COMPLETED = "completed"
    FAILED = "failed"

class Priority(int, Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3

class Task(BaseModel):
    # Works with any enum
    status: Status = Status.PENDING
    priority: Priority = Priority.MEDIUM

task = Task(status=Status.ACTIVE, priority=Priority.HIGH)

# StringEnum (Pydantic v2)
class UserRole(StrEnum):
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

class User(BaseModel):
    role: UserRole = UserRole.GUEST

Configuration

Model Config

from pydantic import BaseModel, ConfigDict
from typing import Optional

# Pydantic v2 style
class User(BaseModel):
    model_config = ConfigDict(
        str_strip_whitespace=True,      # Strip whitespace from strings
        validate_assignment=True,       # Validate on assignment
        arbitrary_types_allowed=True,   # Allow arbitrary types
        use_enum_values=True,           # Use enum values (not enum objects)
        populate_by_name=True,          # Allow population by name (not alias)
        extra='forbid',                 # Forbid extra fields
        frozen=True,                    # Make model immutable
        strict_fields=False,            # Not strict by default
    )
    
    id: int
    name: str
    email: Optional[str] = None

# v1 style (still works)
class UserV1(BaseModel):
    class Config:
        str_strip_whitespace = True
        validate_assignment = True
    
    id: int
    name: str

Field-Level Config

from pydantic import BaseModel, Field, field_config

@field_config(validate_default=True)
def validated_field():
    return Field(default="default_value")

class ModelWithFieldConfig(BaseModel):
    # This field will be validated even with default
    value: str = Field(default="test", validate_default=True)

Advanced Config Patterns

Strict Mode and Extra Fields

from pydantic import BaseModel, ConfigDict, Field
from typing import Any, Optional

# Strict mode with extra allowed
class StrictWithExtra(BaseModel):
    model_config = ConfigDict(
        strict=False,                   # Non-strict mode
        extra="allow"                   # Allow extra fields
    )
    
    id: int
    name: str
    metadata: Optional[dict] = None
    extra_data: Any = Field(default=None)

# This accepts extra fields without error
data = {"id": 1, "name": "John", "extra_field": "allowed"}
user = StrictWithExtra(**data)
print(user.extra_field)  # "allowed"

# Strict mode with forbid (default)
class StrictModel(BaseModel):
    model_config = ConfigDict(strict=True, extra="forbid")
    
    id: int
    name: str
    
    # This will raise ValidationError for extra fields
    data = {"id": 1, "name": "John", "extra": "not allowed"}

Multi-Environment Settings

from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional
import os

class DatabaseSettings(BaseSettings):
    """Database configuration."""
    model_config = SettingsConfigDict(
        env_file='.env.prod',           # Different env file for production
        env_nested_delimiter='__',
    )
    
    host: str = "localhost"
    port: int = 5432
    name: str
    user: str
    password: str

class DevSettings(BaseSettings):
    """Development settings."""
    model_config = SettingsConfigDict(
        env_file='.env.dev',
    )
    
    debug: bool = True
    host: str = "127.0.0.1"
    port: int = 8000

class ProdSettings(BaseSettings):
    """Production settings."""
    model_config = SettingsConfigDict(
        env_file='.env.prod',
    )
    
    debug: bool = False
    host: str = os.getenv("DATABASE_HOST", "db.prod.example.com")
    port: int = 5432

# Use environment-aware settings
env = os.getenv("ENVIRONMENT", "dev")
if env == "prod":
    settings = ProdSettings()
else:
    settings = DevSettings()

# Environment variables:
# For production:
# export ENVIRONMENT=prod
# source .env.prod

# For development:
# export ENVIRONMENT=dev
# source .env.dev

Nested Structure Config

from pydantic import BaseModel, ConfigDict
from typing import Dict, Any, Optional

class APIConfig(BaseModel):
    """Nested API configuration."""
    model_config = ConfigDict(
        extra="allow",
    )
    
    timeout: int = 30
    retries: int = 3
    endpoints: Dict[str, Any] = {}

class Application(BaseModel):
    """Application with nested config structures."""
    model_config = ConfigDict(
        str_strip_whitespace=True,
    )
    
    app_name: str
    version: str
    api: APIConfig = Field(default_factory=APIConfig)
    features: Dict[str, bool] = {}

# Usage
app = Application(
    app_name="MyApp",
    version="1.0.0",
    api={"timeout": 60, "retries": 5, "custom_setting": "extra"}
)

TypeAdapter

TypeAdapter for Custom Validation

from pydantic import TypeAdapter, ValidationError
from typing import List, Optional, Any

# TypeAdapter for custom validation on arbitrary objects
ta_list_str = TypeAdapter(List[str])

# Validate arbitrary data against the adapter
data = ["a", "b", "c"]
validated = ta_list_str.validate_python(data)
print(validated)  # ["a", "b", "c"]

# Validate from JSON
json_data = '["x", "y", "z"]'
validated_json = ta_list_str.validate_json(json_data)
print(validated_json)  # ["x", "y", "z"]

# Convert to JSON
output = ta_list_str.dump_json(["hello", "world"])
print(output)  # b'["hello","world"]'

# Using TypeAdapter for type coercion
ta_int = TypeAdapter(int)
result = ta_int.validate_python("42")  # Converts "42" to 42
print(result)  # 42

# TypeAdapter for complex nested types
from typing import Dict
ta_dict = TypeAdapter(Dict[str, int])
nested_data = {"a": 1, "b": 2, "c": 3}
validated_nested = ta_dict.validate_python(nested_data)

# TypeAdapter for custom error messages
class CustomIntError(BaseException):
    pass

ta_custom = TypeAdapter(int)
try:
    ta_custom.validate_python("not a number")
except ValidationError as e:
    # Custom error handling
    print(f"Validation failed: {e.errors()[0]['msg']}")

RootModel

RootModel for Collection-Only Schemas

from pydantic import RootModel
from typing import List, Optional

# RootModel for collection-only schemas
class StringList(RootModel[List[str]]):
    """Root model for list of strings - no parent wrapper."""
    
    @property
    def labels(self) -> List[str]:
        """Derived property for list items."""
        return [item.upper() for item in self.root]

# Usage with list of strings
strings = StringList(root=["apple", "banana", "cherry"])
print(strings.root)  # ["apple", "banana", "cherry"]
print(strings.labels)  # ["APPLE", "BANANA", "CHERRY"]

# From JSON
json_data = '["dog", "cat", "bird"]'
pets = StringList.model_validate_json(json_data)
print(pets)  # StringList(root=['dog', 'cat', 'bird'])

# Optional root
class OptionalList(RootModel[Optional[List[str]]]):
    """Root model with optional root."""
    pass

optional = OptionalList(root=["a", "b"])
empty = OptionalList(root=None)
print(optional.root)  # ["a", "b"]
print(empty.root)  # None

# Multiple types with RootModel
class NumberList(RootModel[List[int]]):
    """List of numbers."""
    pass

numbers = NumberList(root=[1, 2, 3])
print(numbers.model_dump())  # [1, 2, 3]

Discriminated Unions

Basic Discriminated Union

from pydantic import BaseModel, Field, Discriminator
from typing import Union, Annotated

class Cat(BaseModel):
    pet_type: str = Field(default="cat", const=True)
    meows: int

class Dog(BaseModel):
    pet_type: str = Field(default="dog", const=True)
    barks: float

class Zoo(BaseModel):
    # Using Tag for discriminated union
    pet: Union[Cat, Dog] = Field(..., discriminator='pet_type')

# Pydantic v2 style
class ZooV2(BaseModel):
    pet: Annotated[
        Union[Cat, Dog],
        Discriminator(tag='pet_type')
    ]

# This is how it's done in API responses with multiple types
class ApiResponse(BaseModel):
    status: str
    data: Union[Cat, Dog] = Field(..., discriminator='pet_type')

# Usage
response = ApiResponse(
    status="success",
    data={"pet_type": "cat", "meows": 5}
)
print(response.data)  # Cat(meows=5)

Advanced Discriminated Union

from pydantic import BaseModel, Field, Discriminator
from typing import Union, Literal, Annotated

class Image(BaseModel):
    type: Literal["image"]
    url: str
    format: Literal["jpg", "png", "gif"]

class Video(BaseModel):
    type: Literal["video"]
    url: str
    duration: int
    format: Literal["mp4", "webm"]

class Document(BaseModel):
    type: Literal["document"]
    pages: int
    format: Literal["pdf", "docx"]

# API Response with multiple media types
class MediaResponse(BaseModel):
    id: int
    title: str
    media: Annotated[
        Union[Image, Video, Document],
        Discriminator(tag='type')
    ]
    created_at: str

# Pydantic automatically routes based on discriminator
response = MediaResponse(
    id=1,
    title="My Video",
    media={"type": "video", "url": "https://example.com/video.mp4", "duration": 120, "format": "mp4"},
    created_at="2024-01-01T00:00:00Z"
)
print(response.media)  # Video(duration=120, url='https://example.com/video.mp4', format='mp4')

Error Handling

Error Handling with ValidationError

from pydantic import BaseModel, ValidationError
from typing import List, Optional

# API response handling with error formatting
class UserResponse(BaseModel):
    id: int
    username: str
    email: str
    age: int

# Try/except for API responses
def fetch_user_from_api(api_data):
    try:
        user = UserResponse(**api_data)
        return {"success": True, "data": user}
    except ValidationError as e:
        # Format validation errors for API
        errors = e.errors()
        formatted_errors = {
            error['loc'][0]: {
                'message': error['msg'],
                'type': error['type'],
                'input': error['input']
            }
            for error in errors
            if error['loc'] and len(error['loc']) > 0
        }
        return {
            "success": False,
            "errors": formatted_errors
        }

# Example usage
valid_data = {"id": 1, "username": "john", "email": "john@example.com", "age": 30}
invalid_data = {"id": "not_an_int", "username": "jo", "email": "invalid", "age": -5}

result = fetch_user_from_api(valid_data)
print(result["success"])  # True

result = fetch_user_from_api(invalid_data)
print(result["success"])  # False
print(result["errors"])  # Formatted error dict

# Error details in e.errors()
try:
    user = UserResponse(id="not an int", name="John", age="not an int")
except ValidationError as e:
    # Full error list
    print(e.errors())
    # [
    #     {
    #         'type': 'int_parsing',
    #         'loc': ('id',),
    #         'msg': 'Input should be a valid integer',
    #         'input': 'not an int'
    #     },
    #     {
    #         'type': 'int_parsing',
    #         'loc': ('age',),
    #         'msg': 'Input should be a valid integer',
    #         'input': 'not an int'
    #     }
    # ]
    
    # Human-readable
    print(e)
    # 2 validation errors for UserResponse
    # id
    #   Input should be a valid integer [type=int_parsing, input_value='not an int', input_type=str]
    # age
    #   Input should be a valid integer [type=int_parsing, input_value='not an int', input_type=str]

# Custom error messages in field validators
from pydantic import field_validator

class AgeUser(BaseModel):
    age: int
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v):
        if v < 0:
            raise ValueError('Age must be a positive number')
        if v > 150:
            raise ValueError('Are you really that old?')
        return v

try:
    AgeUser(age=-5)
except ValidationError as e:
    print(e.error_count())  # 1
    for error in e.errors():
        print(f"Field: {error['loc']}")
        print(f"Message: {error['msg']}")
        print(f"Input: {error['input']}")

Settings

BaseSettings

from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional

class Settings(BaseSettings):
    """Application settings from environment variables."""
    
    # Required settings
    app_name: str
    database_url: str
    
    # Optional with defaults
    debug: bool = False
    port: int = 8000
    max_connections: int = 10
    
    # Sensitive settings (will be masked in output)
    secret_key: str
    
    # Settings with aliases
    db_host: str = Field(alias='DATABASE_HOST', default='localhost')
    
    #model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')

# Usage
# Reads from environment variables and .env file
settings = Settings(
    app_name="My App",
    database_url="postgresql://localhost/mydb",
    secret_key="super-secret"
)

# Access values
print(settings.app_name)
print(settings.debug)

# Configuration via model_config
class SettingsV2(BaseSettings):
    model_config = SettingsConfigDict(
        env_file='.env',
        env_file_encoding='utf-8',
        case_sensitive=False,
        extra='ignore'
    )
    
    app_name: str
    debug: bool = False

Nested Settings

from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional

class DatabaseSettings(BaseSettings):
    """Database configuration."""
    host: str = "localhost"
    port: int = 5432
    name: str
    user: str
    password: str

class RedisSettings(BaseSettings):
    """Redis configuration."""
    host: str = "localhost"
    port: int = 6379
    db: int = 0

class Settings(BaseSettings):
    """Application settings."""
    app_name: str
    database: DatabaseSettings
    redis: RedisSettings
    
    model_config = SettingsConfigDict(env_nested_delimiter='__')

# Environment variables:
# APP_NAME=MyApp
# DATABASE__HOST=db.example.com
# DATABASE__PORT=5432
# REDIS__HOST=redis.example.com

settings = Settings(
    app_name="MyApp",
    database={"name": "mydb", "user": "user", "password": "pass"},
    redis={}
)

FastAPI Integration

Request Models

from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel, EmailStr, Field

app = FastAPI()

class UserCreate(BaseModel):
    """User creation schema."""
    username: str = Field(min_length=3, max_length=50)
    email: EmailStr
    password: str = Field(min_length=8)
    age: int = Field(ge=0, le=150)

class UserResponse(BaseModel):
    """User response schema (excludes sensitive data)."""
    id: int
    username: str
    email: str
    
    model_config = {'from_attributes': True}

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
    """Create a new user."""
    # Validate and process user data
    # user is already validated!
    new_user = await save_user(user.model_dump())
    return new_user

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    """Get user by ID."""
    user = await get_user_by_id(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

Response Models

from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime

class Item(BaseModel):
    id: int
    name: str
    price: float

class ItemWithTax(BaseModel):
    """Item with calculated tax."""
    id: int
    name: str
    price: float
    tax: float
    
    @classmethod
    def from_item(cls, item: Item):
        return cls(
            id=item.id,
            name=item.name,
            price=item.price,
            tax=item.price * 0.1  # 10% tax
        )

app = FastAPI()

@app.get("/items/{item_id}", response_model=ItemWithTax)
async def get_item(item_id: int):
    item = await get_item_from_db(item_id)
    return ItemWithTax.from_item(item)

Nested Validation

from fastapi import FastAPI
from pydantic import BaseModel, ValidationError

app = FastAPI()

class Address(BaseModel):
    street: str
    city: str
    country: str

class UserWithAddress(BaseModel):
    name: str
    address: Address

@app.post("/users")
async def create_user(user: UserWithAddress):
    return user

# Nested validation error example:
# Request: {"name": "John", "address": {"street": "123 Main"}}
# Error: Validation error for address.city (field required)

Best Practices

1. Use Type Hints

# Good: Full type hints
class User(BaseModel):
    id: int
    name: str
    email: str
    is_active: bool = True

# Bad: Missing type hints
class User(BaseModel):
    id = None
    name = None

2. Define Defaults Properly

# Good: Use default_factory for mutable objects
class User(BaseModel):
    tags: List[str] = Field(default_factory=list)
    metadata: dict = Field(default_factory=dict)

# Bad: Mutable default argument
class User(BaseModel):
    tags: List[str] = []  # ERROR: mutable default!
    metadata: dict = {}

3. Use Constrained Types

# Good: Constrained types
class User(BaseModel):
    username: str = Field(min_length=3, max_length=20)
    age: int = Field(ge=0, le=150)

# Bad: Unconstrained validation in handler
class User(BaseModel):
    username: str
    age: int
    
    @field_validator('username')
    def validate_username(self, v):
        if len(v) < 3 or len(v) > 20:
            raise ValueError('Invalid username')
        return v

4. Separate Schemas

# Good: Separate schemas for different operations
class UserBase(BaseModel):
    email: EmailStr

class UserCreate(UserBase):
    username: str
    password: str

class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
    username: Optional[str] = None

class UserResponse(UserBase):
    id: int
    created_at: datetime
    
    model_config = {'from_attributes': True}

Common Issues

Performance

# Issue: Slow validation
# Solution: Use Pydantic v2 (much faster)
# pip install pydantic>=2.0.0

# Solution: Use orjson for serialization
# pip install orjson

import orjson

class FastModel(BaseModel):
    model_config = {'json_loads': orjson.loads, 'json_dumps': orjson.dumps}

Mutable Defaults

# Issue: Mutable default arguments
# Error in Pydantic v2
class BadModel(BaseModel):
    items: list = []  # ERROR!

# Solution: Use default_factory
class GoodModel(BaseModel):
    items: list = Field(default_factory=list)

Validation Order

# Issue: Validator order
# In Pydantic v2, field_validators run in order of definition
# mode='before' validators run first, then field type validation, then 'after'

class OrderedValidation(BaseModel):
    value: str
    
    @field_validator('value', mode='before')
    @classmethod
    def before_validation(cls, v):
        # Runs first
        return v.strip().lower() if isinstance(v, str) else v
    
    @field_validator('value')
    @classmethod
    def after_validation(cls, v):
        # Runs after type validation
        return v

References

  • Official Documentation: https://docs.pydantic.dev/
  • GitHub Repository: https://github.com/pydantic/pydantic
  • Pydantic Discord: https://discord.gg/pydantic
  • FastAPI Documentation: https://fastapi.tiangolo.com/
  • Stack Overflow: https://stackoverflow.com/questions/tagged/pydantic