Python Pydantic

Data validation and serialization with Pydantic models.

BaseModel Fundamentals

Define a model — fields become type-checked attributes with automatic serialization
from pydantic import BaseModel

class Device(BaseModel):
    hostname: str
    ip: str
    vlan: int
    enabled: bool = True  # default value

device = Device(hostname="switch-01", ip="10.50.1.100", vlan=10)
print(device.hostname)       # "switch-01"
print(device.model_dump())   # {"hostname": "switch-01", "ip": "10.50.1.100", "vlan": 10, "enabled": True}

Field with Validation

Field() adds constraints, descriptions, and examples — drives OpenAPI docs in FastAPI
from pydantic import BaseModel, Field

class PolicyRule(BaseModel):
    name: str = Field(
        ...,                          # required (no default)
        min_length=1,
        max_length=128,
        description="Policy set name",
        examples=["Wired_802.1X_Closed"],
    )
    vlan: int = Field(..., ge=1, le=4094, description="VLAN ID")
    priority: int = Field(0, ge=0, le=100)
    mac_address: str = Field(
        ...,
        pattern=r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$",
        description="MAC address in colon-separated format",
    )

Validators

@field_validator for single fields, @model_validator for cross-field logic
from pydantic import BaseModel, field_validator, model_validator

class SubnetConfig(BaseModel):
    network: str
    prefix_len: int
    gateway: str

    @field_validator("prefix_len")
    @classmethod
    def check_prefix(cls, v: int) -> int:
        if not 8 <= v <= 30:
            raise ValueError(f"Prefix length {v} out of range 8-30")
        return v

    @model_validator(mode="after")
    def gateway_in_network(self) -> "SubnetConfig":
        # Cross-field validation -- gateway must be in declared network
        if not self.gateway.startswith(self.network.rsplit(".", 1)[0]):
            raise ValueError("Gateway not in declared network")
        return self

Serialization: model_dump and model_validate

model_dump() to dict, model_validate() from dict — replaces v1 .dict() and .parse_obj()
from pydantic import BaseModel

class Endpoint(BaseModel):
    hostname: str
    ip: str
    vlan: int | None = None

# Dict to model
data = {"hostname": "ap-lobby", "ip": "10.50.2.15"}
endpoint = Endpoint.model_validate(data)

# Model to dict -- exclude unset, rename fields
endpoint.model_dump()                          # all fields including defaults
endpoint.model_dump(exclude_none=True)         # drop None fields
endpoint.model_dump(exclude={"vlan"})          # exclude specific fields
endpoint.model_dump(mode="json")               # JSON-safe types (datetime -> str)

# JSON string round-trip
json_str = endpoint.model_dump_json(indent=2)
restored = Endpoint.model_validate_json(json_str)

Nested Models and Optional Fields

Compose models — Pydantic validates the entire tree recursively
from pydantic import BaseModel

class Interface(BaseModel):
    name: str
    ip: str | None = None
    vlan: int | None = None

class Switch(BaseModel):
    hostname: str
    model: str
    interfaces: list[Interface] = []
    location: str | None = None

switch = Switch(
    hostname="core-sw-01",
    model="C9300-48P",
    interfaces=[
        Interface(name="Gi1/0/1", ip="10.50.1.1", vlan=10),
        Interface(name="Gi1/0/2", vlan=20),
    ],
)
# Nested models are validated on construction -- bad vlan type raises ValidationError

Computed Fields

@computed_field for derived values — included in serialization automatically
from pydantic import BaseModel, computed_field

class CertInfo(BaseModel):
    common_name: str
    domain: str

    @computed_field
    @property
    def fqdn(self) -> str:
        return f"{self.common_name}.{self.domain}"

cert = CertInfo(common_name="ise-01", domain="inside.domusdigitalis.dev")
print(cert.fqdn)          # "ise-01.inside.domusdigitalis.dev"
print(cert.model_dump())  # includes "fqdn" in output

BaseSettings for Configuration

BaseSettings reads from environment variables — 12-factor config pattern
from pydantic_settings import BaseSettings
from pydantic import Field

class Settings(BaseSettings):
    model_config = {"env_prefix": "DOMUS_"}

    db_url: str = Field(..., description="PostgreSQL connection string")
    api_key: str = Field(..., description="API authentication key")
    debug: bool = Field(False, description="Enable debug mode")
    port: int = Field(8000, ge=1024, le=65535)

# Reads DOMUS_DB_URL, DOMUS_API_KEY, DOMUS_DEBUG, DOMUS_PORT from environment
settings = Settings()

Discriminated Unions

Tagged unions with Literal discriminator — Pydantic picks the right model automatically
from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field

class WiredAuth(BaseModel):
    auth_type: Literal["wired"] = "wired"
    port: str
    vlan: int

class WirelessAuth(BaseModel):
    auth_type: Literal["wireless"] = "wireless"
    ssid: str
    band: str

AuthConfig = Annotated[
    Union[WiredAuth, WirelessAuth],
    Field(discriminator="auth_type"),
]

class EndpointPolicy(BaseModel):
    mac: str
    auth: AuthConfig

# Pydantic routes to correct model based on auth_type value
policy = EndpointPolicy.model_validate({
    "mac": "AA:BB:CC:DD:EE:FF",
    "auth": {"auth_type": "wired", "port": "Gi1/0/1", "vlan": 10},
})