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},
})