Skip to content

Pydantic — Advanced Patterns

Discriminated Unions

Use a literal field to select the correct model variant — faster and more predictable than untagged unions.

from typing import Annotated, Literal, Union

from pydantic import BaseModel, Field


class CreditCard(BaseModel):
    method: Literal["credit_card"]
    card_number: str
    cvv: str


class BankTransfer(BaseModel):
    method: Literal["bank_transfer"]
    account_number: str
    routing_number: str


Payment = Annotated[
    Union[CreditCard, BankTransfer],
    Field(discriminator="method"),
]


class Order(BaseModel):
    id: int
    payment: Payment


order = Order.model_validate({
    "id": 1,
    "payment": {"method": "credit_card", "card_number": "4111...", "cvv": "123"},
})

Custom Discriminator Function

from typing import Annotated, Any, Union

from pydantic import BaseModel, Discriminator, Tag


def detect_shape(v: Any) -> str:
    if isinstance(v, dict) and "radius" in v:
        return "circle"
    return "rect"


class Circle(BaseModel):
    radius: float


class Rect(BaseModel):
    width: float
    height: float


Shape = Annotated[
    Union[Annotated[Circle, Tag("circle")], Annotated[Rect, Tag("rect")]],
    Discriminator(detect_shape),
]

Generic Models

from typing import Generic, TypeVar

from pydantic import BaseModel

T = TypeVar("T")


class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int
    per_page: int


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


users_page = Page[User].model_validate({
    "items": [{"id": 1, "name": "Alice"}],
    "total": 50,
    "page": 1,
    "per_page": 25,
})

TypeAdapter

Validate and serialize non-model types — lists, dicts, unions, primitives.

from pydantic import TypeAdapter

int_adapter = TypeAdapter(int)
int_adapter.validate_python("42")         # 42
int_adapter.validate_json(b'"42"')        # 42

list_adapter = TypeAdapter(list[int])
list_adapter.validate_python(["1", "2"])  # [1, 2]
list_adapter.json_schema()                # {"items": {"type": "integer"}, ...}

BaseSettings — Config from Environment

from pydantic import Field
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class AppSettings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env")

    debug: bool = False
    db_url: str = Field(alias="DATABASE_URL")
    secret_key: str
    workers: int = 4
    allowed_hosts: list[str] = Field(default_factory=lambda: ["localhost"])
APP_DEBUG=true
DATABASE_URL=postgresql://user:pass@localhost/db
APP_SECRET_KEY=super-secret

Nested Settings

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict


class DatabaseConfig(BaseModel):
    host: str = "localhost"
    port: int = 5432


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", env_nested_delimiter="__")
    db: DatabaseConfig = DatabaseConfig()
APP_DB__HOST=db.prod.internal
APP_DB__PORT=5433

JSON Schema, Inheritance & Private Attrs

from pydantic import BaseModel, Field, PrivateAttr


class Item(BaseModel):
    name: str = Field(title="Item Name", description="Display name", examples=["Widget"])

Item.model_json_schema()  # full JSON Schema dict


class BaseUser(BaseModel):
    name: str
    email: str

class AdminUser(BaseUser):  # inherits all fields and validators
    permissions: list[str]


class Service(BaseModel):
    name: str
    _client: object = PrivateAttr(default=None)  # excluded from validation/schema