|$ 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

PythonPydanticValidationIntermediate🎯Free Tools
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
1from pydantic import BaseModel, Field, EmailStr, field_validator
2from typing import Annotated
3
4class 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
19u = User.model_validate({"id": 1, "name": " Ada ", "email": "ada@example.com"})
20print(u.model_dump())
21print(u.model_dump_json(indent=2))
model_validate & Modes
validate.py
Python
1from pydantic import BaseModel, ConfigDict, ValidationError
2
3class Item(BaseModel):
4 model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
5 sku: str
6 price: float
7
8try:
9 Item.model_validate({"sku": " A ", "price": "9.99", "nope": 1})
10except ValidationError as e:
11 print(e.error_count())
12 print(e.errors())
13
14item = Item.model_validate_json('{"sku": "A", "price": 1.5}')
15print(item)
16# From ORM objects:
17# Item.model_validate(orm_obj, from_attributes=True)
Nested Models & Unions
nested.py
Python
1from pydantic import BaseModel, Field
2from typing import Literal, Annotated
3from pydantic import TypeAdapter
4
5class Click(BaseModel):
6 kind: Literal["click"]
7 x: int
8 y: int
9
10class Key(BaseModel):
11 kind: Literal["key"]
12 key: str
13
14Event = Annotated[Click | Key, Field(discriminator="kind")]
15adapter = TypeAdapter(Event)
16print(adapter.validate_python({"kind": "click", "x": 1, "y": 2}))
Settings
settings.py
Python
1from pydantic import Field
2from pydantic_settings import BaseSettings, SettingsConfigDict
3
4class 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
11settings = Settings() # reads APP_HOST, APP_PORT, ...
12print(settings.host, settings.port)
JSON Schema
schema.py
Python
1from pydantic import BaseModel, Field
2
3class Product(BaseModel):
4 name: str = Field(json_schema_extra={"examples": ["Widget"]})
5 price: float = Field(gt=0)
6
7print(Product.model_json_schema())
Custom Serializers
ser.py
Python
1from datetime import datetime, timezone
2from pydantic import BaseModel, field_serializer
3
4class 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
10print(Event(at=datetime.now(timezone.utc)).model_dump())
v1 → v2 Migration Cheatsheet
v1v2
parse_objmodel_validate
parse_raw / parse_jsonmodel_validate_json
.dict()model_dump()
.json()model_dump_json()
class Configmodel_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
1from pydantic import BaseModel, model_validator, Field
2
3class 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
13print(Range(start=1, end=3))
14# Range(start=5, end=1) # ValidationError
Computed Fields
computed.py
Python
1from pydantic import BaseModel, computed_field
2
3class 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
12print(Box(width=3, height=4).model_dump())
Aliases & Populate by Name
aliases.py
Python
1from pydantic import BaseModel, ConfigDict, Field
2
3class 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
8p = Person.model_validate({"fullName": "Ada", "ageYears": 36})
9print(p.full_name)
10print(p.model_dump(by_alias=True))
TypeAdapter for Non-Models
adapter.py
Python
1from pydantic import TypeAdapter, Field
2from typing import Annotated
3
4PositiveIntList = TypeAdapter(list[Annotated[int, Field(gt=0)]])
5print(PositiveIntList.validate_python([1, 2, 3]))
6# PositiveIntList.validate_python([1, -1]) # error
7
8JsonDict = TypeAdapter(dict[str, str])
9print(JsonDict.validate_json('{"a": "b"}'))
Pydantic Dataclasses
pyd_dc.py
Python
1from pydantic.dataclasses import dataclass
2from pydantic import Field
3
4@dataclass
5class Item:
6 name: str
7 qty: int = Field(gt=0)
8
9item = Item(name="x", qty=2)
10print(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
QuestionAnswer
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
1from typing import Annotated
2from pydantic import BaseModel, BeforeValidator, AfterValidator, Field
3
4def strip(v: str) -> str:
5 return v.strip()
6
7def non_empty(v: str) -> str:
8 if not v:
9 raise ValueError("empty")
10 return v
11
12Name = Annotated[str, BeforeValidator(strip), AfterValidator(non_empty), Field(max_length=50)]
13
14class Form(BaseModel):
15 name: Name
16
17print(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.