|$ curl https://forge-ai.dev/api/markdown?path=docs/python/pydantic
$cat docs/python-pydantic-v2.md
updated Today·20-26 min read·published
Python Pydantic v2
Introduction
Pydantic v2 is the standard runtime validation layer for Python APIs. Models parse untrusted data into typed objects, emit JSON Schema, and integrate with FastAPI.
ℹ
info
Install: pip install pydantic pydantic-settings. Prefer v2 APIs: model_validate, model_dump.
Models & Field
models.py
Python
| 1 | from pydantic import BaseModel, Field, EmailStr, field_validator |
| 2 | from typing import Annotated |
| 3 | |
| 4 | class User(BaseModel): |
| 5 | id: int = Field(gt=0) |
| 6 | name: str = Field(min_length=1, max_length=100) |
| 7 | email: EmailStr |
| 8 | age: int | None = Field(default=None, ge=0, le=150) |
| 9 | tags: list[str] = Field(default_factory=list) |
| 10 | |
| 11 | @field_validator("name") |
| 12 | @classmethod |
| 13 | def strip_name(cls, v: str) -> str: |
| 14 | v = v.strip() |
| 15 | if not v: |
| 16 | raise ValueError("empty name") |
| 17 | return v |
| 18 | |
| 19 | u = User.model_validate({"id": 1, "name": " Ada ", "email": "ada@example.com"}) |
| 20 | print(u.model_dump()) |
| 21 | print(u.model_dump_json(indent=2)) |
model_validate & Modes
validate.py
Python
| 1 | from pydantic import BaseModel, ConfigDict, ValidationError |
| 2 | |
| 3 | class Item(BaseModel): |
| 4 | model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") |
| 5 | sku: str |
| 6 | price: float |
| 7 | |
| 8 | try: |
| 9 | Item.model_validate({"sku": " A ", "price": "9.99", "nope": 1}) |
| 10 | except ValidationError as e: |
| 11 | print(e.error_count()) |
| 12 | print(e.errors()) |
| 13 | |
| 14 | item = Item.model_validate_json('{"sku": "A", "price": 1.5}') |
| 15 | print(item) |
| 16 | # From ORM objects: |
| 17 | # Item.model_validate(orm_obj, from_attributes=True) |
Nested Models & Unions
nested.py
Python
| 1 | from pydantic import BaseModel, Field |
| 2 | from typing import Literal, Annotated |
| 3 | from pydantic import TypeAdapter |
| 4 | |
| 5 | class Click(BaseModel): |
| 6 | kind: Literal["click"] |
| 7 | x: int |
| 8 | y: int |
| 9 | |
| 10 | class Key(BaseModel): |
| 11 | kind: Literal["key"] |
| 12 | key: str |
| 13 | |
| 14 | Event = Annotated[Click | Key, Field(discriminator="kind")] |
| 15 | adapter = TypeAdapter(Event) |
| 16 | print(adapter.validate_python({"kind": "click", "x": 1, "y": 2})) |
Settings
settings.py
Python
| 1 | from pydantic import Field |
| 2 | from pydantic_settings import BaseSettings, SettingsConfigDict |
| 3 | |
| 4 | class Settings(BaseSettings): |
| 5 | model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_") |
| 6 | host: str = "127.0.0.1" |
| 7 | port: int = Field(default=8000, ge=1, le=65535) |
| 8 | debug: bool = False |
| 9 | database_url: str = "sqlite:///./app.db" |
| 10 | |
| 11 | settings = Settings() # reads APP_HOST, APP_PORT, ... |
| 12 | print(settings.host, settings.port) |
JSON Schema
schema.py
Python
| 1 | from pydantic import BaseModel, Field |
| 2 | |
| 3 | class Product(BaseModel): |
| 4 | name: str = Field(json_schema_extra={"examples": ["Widget"]}) |
| 5 | price: float = Field(gt=0) |
| 6 | |
| 7 | print(Product.model_json_schema()) |
Custom Serializers
ser.py
Python
| 1 | from datetime import datetime, timezone |
| 2 | from pydantic import BaseModel, field_serializer |
| 3 | |
| 4 | class Event(BaseModel): |
| 5 | at: datetime |
| 6 | @field_serializer("at") |
| 7 | def ser_at(self, v: datetime) -> str: |
| 8 | return v.astimezone(timezone.utc).isoformat() |
| 9 | |
| 10 | print(Event(at=datetime.now(timezone.utc)).model_dump()) |
v1 → v2 Migration Cheatsheet
| v1 | v2 |
|---|---|
| parse_obj | model_validate |
| parse_raw / parse_json | model_validate_json |
| .dict() | model_dump() |
| .json() | model_dump_json() |
| class Config | model_config = ConfigDict(...) |
| @validator | @field_validator |
| @root_validator | @model_validator |
Pitfalls
- Mutable defaults: use Field(default_factory=list)
- extra='ignore' can hide typos — prefer forbid at boundaries
- Validate at the edge; trust internal models
- EmailStr needs email-validator package
model_validator
model_val.py
Python
| 1 | from pydantic import BaseModel, model_validator, Field |
| 2 | |
| 3 | class Range(BaseModel): |
| 4 | start: int |
| 5 | end: int = Field(ge=0) |
| 6 | |
| 7 | @model_validator(mode="after") |
| 8 | def check_range(self): |
| 9 | if self.end < self.start: |
| 10 | raise ValueError("end must be >= start") |
| 11 | return self |
| 12 | |
| 13 | print(Range(start=1, end=3)) |
| 14 | # Range(start=5, end=1) # ValidationError |
Computed Fields
computed.py
Python
| 1 | from pydantic import BaseModel, computed_field |
| 2 | |
| 3 | class Box(BaseModel): |
| 4 | width: float |
| 5 | height: float |
| 6 | |
| 7 | @computed_field |
| 8 | @property |
| 9 | def area(self) -> float: |
| 10 | return self.width * self.height |
| 11 | |
| 12 | print(Box(width=3, height=4).model_dump()) |
Aliases & Populate by Name
aliases.py
Python
| 1 | from pydantic import BaseModel, ConfigDict, Field |
| 2 | |
| 3 | class Person(BaseModel): |
| 4 | model_config = ConfigDict(populate_by_name=True) |
| 5 | full_name: str = Field(alias="fullName") |
| 6 | age_years: int = Field(alias="ageYears") |
| 7 | |
| 8 | p = Person.model_validate({"fullName": "Ada", "ageYears": 36}) |
| 9 | print(p.full_name) |
| 10 | print(p.model_dump(by_alias=True)) |
TypeAdapter for Non-Models
adapter.py
Python
| 1 | from pydantic import TypeAdapter, Field |
| 2 | from typing import Annotated |
| 3 | |
| 4 | PositiveIntList = TypeAdapter(list[Annotated[int, Field(gt=0)]]) |
| 5 | print(PositiveIntList.validate_python([1, 2, 3])) |
| 6 | # PositiveIntList.validate_python([1, -1]) # error |
| 7 | |
| 8 | JsonDict = TypeAdapter(dict[str, str]) |
| 9 | print(JsonDict.validate_json('{"a": "b"}')) |
Pydantic Dataclasses
pyd_dc.py
Python
| 1 | from pydantic.dataclasses import dataclass |
| 2 | from pydantic import Field |
| 3 | |
| 4 | @dataclass |
| 5 | class Item: |
| 6 | name: str |
| 7 | qty: int = Field(gt=0) |
| 8 | |
| 9 | item = Item(name="x", qty=2) |
| 10 | print(item) |
Performance Tips
- Reuse model classes; avoid dynamic model creation in hot loops
- model_validate_json can be faster than loads + validate
- Use TypeAdapter for simple shapes without BaseModel overhead
- Prefer strict=False (default) at HTTP edges for coercion
FAQ
| Question | Answer |
|---|---|
| Replace dataclasses? | Use both: dataclass internal, Pydantic at edges |
| With mypy? | pydantic.mypy plugin helps |
| ORM mode? | from_attributes=True in ConfigDict |
| Custom types? | Annotated + BeforeValidator / AfterValidator |
Annotated Validators
annotated_val.py
Python
| 1 | from typing import Annotated |
| 2 | from pydantic import BaseModel, BeforeValidator, AfterValidator, Field |
| 3 | |
| 4 | def strip(v: str) -> str: |
| 5 | return v.strip() |
| 6 | |
| 7 | def non_empty(v: str) -> str: |
| 8 | if not v: |
| 9 | raise ValueError("empty") |
| 10 | return v |
| 11 | |
| 12 | Name = Annotated[str, BeforeValidator(strip), AfterValidator(non_empty), Field(max_length=50)] |
| 13 | |
| 14 | class Form(BaseModel): |
| 15 | name: Name |
| 16 | |
| 17 | print(Form(name=" Ada ")) |
$Blueprint — Engineering Documentation·Section ID: PYTHON-PYDANTIC·Revision: 1.0
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.