AgentSkillsCN

Pydantic Patterns

Pydantic 模式设计

SKILL.md

Pydantic Patterns

Type-safe data validation with Pydantic v2

PRINCIPLES

  1. Schema-First: Design models before implementation
  2. Validation Built-in: Leverage automatic type coercion and validation
  3. Serialization Control: Customize JSON encoding/decoding
  4. Settings Management: Use pydantic-settings for configuration
  5. Performance: Use model_config for optimization

MODEL BASICS

Basic Model Definition

python
from pydantic import BaseModel, Field, ConfigDict
from datetime import datetime
from typing import Annotated

class User(BaseModel):
    """User model with validation."""
    model_config = ConfigDict(
        str_strip_whitespace=True,  # Strip whitespace from strings
        validate_assignment=True,    # Validate on attribute assignment
        extra="forbid",              # Forbid extra fields
    )
    
    id: int
    username: str = Field(..., min_length=3, max_length=50)
    email: str = Field(..., pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
    full_name: str | None = None
    is_active: bool = True
    created_at: datetime = Field(default_factory=datetime.utcnow)

FIELD VALIDATION

Field Constraints

python
from pydantic import Field, PositiveInt, constr, conint

class Product(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    price: float = Field(..., gt=0, description="Price in USD")
    quantity: PositiveInt = Field(default=1)
    sku: constr(pattern=r"^[A-Z]{3}-\d{6}$") = Field(...)
    rating: conint(ge=1, le=5) | None = None

# Using Annotated for reusable constraints
from typing import Annotated

PositiveFloat = Annotated[float, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]

class Order(BaseModel):
    total: PositiveFloat
    customer_name: NonEmptyStr

CUSTOM VALIDATORS

Field and Model Validators

python
from pydantic import BaseModel, field_validator, model_validator
import re

class UserCreate(BaseModel):
    username: str
    password: str
    password_confirm: str
    
    @field_validator("username")
    @classmethod
    def username_alphanumeric(cls, v: str) -> str:
        if not re.match(r"^[a-zA-Z0-9_]+$", v):
            raise ValueError("Username must be alphanumeric")
        return v.lower()
    
    @field_validator("password")
    @classmethod
    def password_strength(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError("Password must be at least 8 characters")
        if not any(c.isupper() for c in v):
            raise ValueError("Password must contain uppercase letter")
        if not any(c.isdigit() for c in v):
            raise ValueError("Password must contain a digit")
        return v
    
    @model_validator(mode="after")
    def passwords_match(self) -> "UserCreate":
        if self.password != self.password_confirm:
            raise ValueError("Passwords do not match")
        return self

# Before validator for transformation
class NormalizedInput(BaseModel):
    tags: list[str]
    
    @field_validator("tags", mode="before")
    @classmethod
    def split_tags(cls, v):
        if isinstance(v, str):
            return [t.strip().lower() for t in v.split(",")]
        return v

NESTED MODELS

Composition Patterns

python
from pydantic import BaseModel
from datetime import datetime

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

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

class Employee(BaseModel):
    id: int
    name: str
    company: Company
    addresses: list[Address] = []

# Self-referencing models
from typing import ForwardRef

class TreeNode(BaseModel):
    value: str
    children: list["TreeNode"] = []

TreeNode.model_rebuild()  # Required for forward references

SERIALIZATION

Model Export Control

python
from pydantic import BaseModel, Field, ConfigDict
from datetime import datetime

class UserInDB(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    email: str
    hashed_password: str = Field(exclude=True)  # Never serialize
    created_at: datetime
    
    # Computed fields
    @property
    def email_domain(self) -> str:
        return self.email.split("@")[1]

# Serialization options
user = UserInDB(id=1, email="test@example.com", hashed_password="xxx", created_at=datetime.now())

# Exclude fields
user.model_dump(exclude={"hashed_password"})

# Include only specific fields
user.model_dump(include={"id", "email"})

# JSON serialization with custom encoder
user.model_dump_json(indent=2)

# From ORM objects
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class UserModel(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    email = Column(String)

# Works with from_attributes=True
user_schema = UserInDB.model_validate(user_model)

SETTINGS MANAGEMENT

Environment Configuration

python
from pydantic import Field, PostgresDsn, RedisDsn
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
        extra="ignore",
    )
    
    # App settings
    app_name: str = "MyAPI"
    debug: bool = False
    
    # Database
    database_url: PostgresDsn
    db_pool_size: int = Field(default=5, ge=1, le=100)
    
    # Redis
    redis_url: RedisDsn | None = None
    
    # Security
    secret_key: str = Field(..., min_length=32)
    access_token_expire_minutes: int = 30
    
    # Nested settings using env prefix
    class Config:
        env_nested_delimiter = "__"

# Usage
settings = Settings()

# .env file:
# DATABASE_URL=postgresql://user:pass@localhost/db
# SECRET_KEY=your-32-character-secret-key-here
# DEBUG=true

GENERIC MODELS

Reusable Patterns

python
from pydantic import BaseModel
from typing import Generic, TypeVar

T = TypeVar("T")

class PaginatedResponse(BaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int
    page_size: int
    pages: int

class APIResponse(BaseModel, Generic[T]):
    success: bool = True
    data: T | None = None
    error: str | None = None

# Usage
class User(BaseModel):
    id: int
    name: str

response: PaginatedResponse[User] = PaginatedResponse(
    items=[User(id=1, name="Alice")],
    total=1,
    page=1,
    page_size=10,
    pages=1,
)

ANTI-PATTERNS

AVOID:

python
# Mutable default values
class Bad(BaseModel):
    items: list = []  # Shared between instances!

# No validation
class NoValidation(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    data: any  # Loses type safety

PREFER:

python
# Factory defaults
class Good(BaseModel):
    items: list[str] = Field(default_factory=list)

# Strict typing
class StrictModel(BaseModel):
    data: dict[str, int]  # Full type information