Pydantic validates data with Python type hints. pip install pydantic. Define: class User(BaseModel): id: int; name: str; email: str. Parse: User.model_validate({"id": 1, "name": "Alice", "email": "[email protected]"}). From JSON: User.model_validate_json('{"id":1,...}'). Serialize: user.model_dump(), user.model_dump(exclude_none=True), user.model_dump_json(). Field: name: str = Field(min_length=1, max_length=50, description="Display name"). Default: status: str = "active". Optional: nickname: str | None = None. Alias: Field(alias="user_name"). populate_by_name=True in ConfigDict to allow both names. Validation: @field_validator("email") @classmethod def validate_email(cls, v): .... @model_validator(mode="after") def check_dates(self): .... Computed: @computed_field @property def full_name(self) -> str: return f"{self.first} {self.last}". Nested: class Order(BaseModel): user: User; lines: list[OrderLine]. RootModel: class Tags(RootModel[list[str]]): .... TypeAdapter: ta = TypeAdapter(list[User]); ta.validate_python([...]). Strict: ConfigDict(strict=True) — no coercion (int “1” → error). model_json_schema() — export JSON Schema. Discriminated union: body: Annotated[Cat|Dog, Field(discriminator="type")]. Settings: pip install pydantic-settings. class Settings(BaseSettings): db_url: str; model_config = SettingsConfigDict(env_file=".env"). settings.model_config["env_prefix"] = "APP_". @cached_property for singleton settings. Custom types: Annotated[int, Field(ge=0, le=100)] or class PositiveInt(int): .... Secret: SecretStr, SecretBytes — redacted in repr. model_rebuild() — resolve forward references. model_copy(update={...}) — immutable update. ValidationError.errors() — structured list of failures with loc, msg, type. Claude Code generates Pydantic schemas, BaseSettings configurations, API request/response models, and discriminated union parsers.
CLAUDE.md for Pydantic
## Pydantic Stack
- Version: pydantic >= 2.7 | pip install "pydantic[email]" pydantic-settings
- Model: class M(BaseModel): field: type = Field(default, constraints...)
- Parse: M.model_validate(dict) | M.model_validate_json(str)
- Dump: m.model_dump() | m.model_dump(exclude_none=True, by_alias=True)
- Validate: @field_validator | @model_validator(mode="after")
- Config: model_config = ConfigDict(strict=True, populate_by_name=True)
- Settings: class S(BaseSettings): model_config = SettingsConfigDict(env_file=".env")
Pydantic Validation Pipeline
# src/schemas.py — Pydantic v2 schema and settings patterns
from __future__ import annotations
import re
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
from typing import Annotated, Any, Literal
from pydantic import (
AnyHttpUrl,
BaseModel,
ConfigDict,
EmailStr,
Field,
SecretStr,
computed_field,
field_validator,
model_validator,
)
from pydantic import TypeAdapter
from pydantic import RootModel
# ─────────────────────────────────────────────────────────────────────────────
# Annotated type aliases — reusable constrained types
# ─────────────────────────────────────────────────────────────────────────────
PositiveInt = Annotated[int, Field(gt=0)]
Percentage = Annotated[float, Field(ge=0.0, le=100.0)]
NonEmptyStr = Annotated[str, Field(min_length=1, strip_whitespace=True)]
ShortStr = Annotated[str, Field(min_length=1, max_length=100, strip_whitespace=True)]
# ─────────────────────────────────────────────────────────────────────────────
# Enum
# ─────────────────────────────────────────────────────────────────────────────
class UserRole(str, Enum):
USER = "user"
MODERATOR = "moderator"
ADMIN = "admin"
class OrderStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# ─────────────────────────────────────────────────────────────────────────────
# Nested models
# ─────────────────────────────────────────────────────────────────────────────
class Address(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
street: NonEmptyStr
city: NonEmptyStr
state: Annotated[str, Field(min_length=2, max_length=2, pattern=r"^[A-Z]{2}$")]
postal_code: Annotated[str, Field(pattern=r"^\d{5}(-\d{4})?$")]
country: Annotated[str, Field(default="US", max_length=2)]
@field_validator("state", "country", mode="before")
@classmethod
def uppercase_codes(cls, v: str) -> str:
return v.upper()
class UserBase(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
populate_by_name=True, # allow both field_name and alias
use_enum_values=True, # store "admin" not UserRole.ADMIN
)
first_name: ShortStr = Field(alias="firstName")
last_name: ShortStr = Field(alias="lastName")
email: EmailStr
role: UserRole = UserRole.USER
@computed_field
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
class UserCreate(UserBase):
"""Schema for POST /users — includes password input."""
password: Annotated[str, Field(min_length=8, max_length=72)]
address: Address | None = None
@field_validator("password")
@classmethod
def password_complexity(cls, v: str) -> str:
errors = []
if not re.search(r"[A-Z]", v):
errors.append("one uppercase letter")
if not re.search(r"[0-9]", v):
errors.append("one digit")
if errors:
raise ValueError(f"Password must contain: {', '.join(errors)}")
return v
class UserResponse(UserBase):
"""Schema for API response — no password, adds id and timestamps."""
id: PositiveInt
is_active: bool = True
created_at: datetime
address: Address | None = None
class UserUpdate(BaseModel):
"""Partial update — all fields optional."""
model_config = ConfigDict(str_strip_whitespace=True, use_enum_values=True)
first_name: ShortStr | None = Field(None, alias="firstName")
last_name: ShortStr | None = Field(None, alias="lastName")
role: UserRole | None = None
address: Address | None = None
# ─────────────────────────────────────────────────────────────────────────────
# Order models with model_validator
# ─────────────────────────────────────────────────────────────────────────────
class OrderLine(BaseModel):
product_id: PositiveInt
quantity: Annotated[int, Field(ge=1, le=1000)]
unit_price: Annotated[Decimal, Field(gt=Decimal("0"), decimal_places=2)]
@computed_field
@property
def subtotal(self) -> Decimal:
return self.quantity * self.unit_price
class OrderCreate(BaseModel):
user_id: PositiveInt
lines: Annotated[list[OrderLine], Field(min_length=1)]
shipping_address: Address
coupon_code: str | None = None
notes: Annotated[str, Field(max_length=500)] | None = None
@model_validator(mode="after")
def lines_not_empty(self) -> "OrderCreate":
if not self.lines:
raise ValueError("Order must have at least one line")
return self
@computed_field
@property
def total(self) -> Decimal:
return sum(line.subtotal for line in self.lines)
class OrderResponse(BaseModel):
id: PositiveInt
user_id: PositiveInt
lines: list[OrderLine]
status: OrderStatus
shipping_address: Address
total: Decimal
created_at: datetime
# ─────────────────────────────────────────────────────────────────────────────
# Discriminated unions — polymorphic event types
# ─────────────────────────────────────────────────────────────────────────────
class UserCreatedEvent(BaseModel):
type: Literal["user_created"]
user_id: PositiveInt
email: EmailStr
occurred_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class OrderPlacedEvent(BaseModel):
type: Literal["order_placed"]
order_id: PositiveInt
user_id: PositiveInt
total: Decimal
occurred_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class PaymentReceivedEvent(BaseModel):
type: Literal["payment_received"]
order_id: PositiveInt
amount: Decimal
currency: Annotated[str, Field(min_length=3, max_length=3)]
occurred_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
# Discriminated union — Pydantic uses the "type" field to pick the right model
DomainEvent = Annotated[
UserCreatedEvent | OrderPlacedEvent | PaymentReceivedEvent,
Field(discriminator="type"),
]
class EventEnvelope(BaseModel):
event_id: str
payload: DomainEvent
# ─────────────────────────────────────────────────────────────────────────────
# RootModel — typed list/dict wrapper
# ─────────────────────────────────────────────────────────────────────────────
class UserList(RootModel[list[UserResponse]]):
"""Wraps list[UserResponse] for validation and serialization."""
pass
class ProductCatalog(RootModel[dict[str, Decimal]]):
"""Maps SKU → price."""
pass
# ─────────────────────────────────────────────────────────────────────────────
# TypeAdapter — validate without defining a new model
# ─────────────────────────────────────────────────────────────────────────────
# Validate a list of emails without a full model:
email_list_adapter = TypeAdapter(list[EmailStr])
def parse_email_list(raw: list[str]) -> list[str]:
return email_list_adapter.validate_python(raw)
# ─────────────────────────────────────────────────────────────────────────────
# BaseSettings — environment variable loading
# ─────────────────────────────────────────────────────────────────────────────
try:
from pydantic_settings import BaseSettings, SettingsConfigDict
class DatabaseSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="DB_",
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
host: str = "localhost"
port: int = 5432
name: str = "mydb"
user: str = "app"
password: SecretStr
@property
def url(self) -> str:
return (
f"postgresql://{self.user}:{self.password.get_secret_value()}"
f"@{self.host}:{self.port}/{self.name}"
)
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
debug: bool = False
log_level: str = "INFO"
allowed_hosts: list[str] = ["localhost"]
api_base_url: AnyHttpUrl = "http://localhost:8000" # type: ignore[assignment]
secret_key: SecretStr
db: DatabaseSettings = DatabaseSettings()
model_config = SettingsConfigDict(
env_nested_delimiter="__", # DB__HOST → db.host
env_file=".env",
)
# Singleton pattern — load once, reuse everywhere
_settings: AppSettings | None = None
def get_settings() -> AppSettings:
global _settings
if _settings is None:
_settings = AppSettings() # type: ignore[call-arg]
return _settings
except ImportError:
pass # pydantic-settings not installed
if __name__ == "__main__":
# UserCreate validation
try:
u = UserCreate.model_validate({
"firstName": "Alice",
"lastName": "Smith",
"email": "[email protected]",
"password": "SecurePass1",
"address": {
"street": "123 Main St",
"city": "Springfield",
"state": "il", # auto-upcased to IL
"postal_code": "62701",
},
})
print(f"User: {u.full_name} <{u.email}>")
print(f"State: {u.address.state}") # IL
except Exception as e:
print(f"Validation error: {e}")
# Discriminated union parsing
envelope = EventEnvelope.model_validate({
"event_id": "evt-001",
"payload": {
"type": "order_placed",
"order_id": 42,
"user_id": 1,
"total": "99.99",
},
})
print(f"Event type: {type(envelope.payload).__name__}") # OrderPlacedEvent
# JSON schema generation
schema = OrderCreate.model_json_schema()
print(f"Schema title: {schema['title']}")
For the dataclasses alternative — Python dataclasses store data in typed fields but do no validation: @dataclass class User: id: int will happily accept User(id="not-an-int") and silently store a string, while class User(BaseModel): id: int raises ValidationError with a human-readable loc path and msg at parse time, model_dump(by_alias=True) serializes camelCase for JSON APIs, and model_json_schema() outputs OpenAPI-compatible JSON Schema that FastAPI serves at /openapi.json automatically. For the marshmallow alternative — marshmallow requires separate Schema and Model classes plus an explicit many=True flag for lists, while Pydantic’s TypeAdapter(list[User]).validate_python(data) validates a list without a separate schema class, @computed_field adds derived properties to the serialized output without extra code, and BaseSettings with env_nested_delimiter="__" loads DB__HOST from the environment into settings.db.host with full type coercion and .env file support. The Claude Skills 360 bundle includes Pydantic skill sets covering BaseModel field definitions, Field constraints and aliases, field_validator and model_validator hooks, ConfigDict strict mode, computed_field properties, discriminated unions, RootModel and TypeAdapter, model_dump serialization options, BaseSettings environment loading, env_nested_delimiter for nested config, SecretStr for sensitive values, and model_json_schema for OpenAPI. Start with the free tier to try data validation code generation.