Pydantic v2: validate data at the edges of your Python application
Pydantic turns type hints into fast runtime validation and serialisation. Here are the core patterns for API payloads, settings and LLM outputs, plus the v2 changes that trip people up.
Author's connection to this tool: No connection. An independent overview written by the RecallRun editors.
Most bugs that reach production come in through the edges of a system: an API request with a missing field, a config value that's a string instead of a number, a third-party webhook that changed shape, or an LLM that returned almost-valid JSON. Pydantic lets you describe the data you expect with ordinary Python type hints and validates it at runtime, with clear error messages. Version 2 rewrote the core in Rust (pydantic-core), making validation much faster.
Models from type hints
from datetime import datetime
from pydantic import BaseModel, Field, EmailStr # EmailStr needs: pip install "pydantic[email]"
class OrderItem(BaseModel):
sku: str = Field(min_length=3)
qty: int = Field(gt=0, le=100)
class Order(BaseModel):
customer_email: EmailStr
items: list[OrderItem]
created_at: datetime
note: str | None = None
order = Order.model_validate({
"customer_email": "[email protected]",
"items": [{"sku": "BOOK-42", "qty": "2"}], # "2" is converted to int 2
"created_at": "2026-10-04T10:15:00Z",
})
print(order.items[0].qty + 1) # 3
Invalid data raises a ValidationError that lists every problem with its location, such as items.0.qty: Input should be greater than 0, which you can return to API clients directly.
Custom rules
from pydantic import field_validator, model_validator
class Signup(BaseModel):
username: str
password: str
password_again: str
@field_validator("username")
@classmethod
def lowercase_slug(cls, v: str) -> str:
if not v.replace("-", "").isalnum():
raise ValueError("letters, numbers and dashes only")
return v.lower()
@model_validator(mode="after")
def passwords_match(self):
if self.password != self.password_again:
raise ValueError("passwords do not match")
return self
Serialising
order.model_dump() # dict
order.model_dump_json(exclude_none=True)
Order.model_validate_json(raw_bytes) # parse and validate JSON in one fast step
Strict vs lax mode
By default Pydantic converts compatible types (the string "2" becomes 2). For payloads where silent conversion could hide a bug, use strict mode, per field or per model:
from pydantic import ConfigDict
class Payment(BaseModel):
model_config = ConfigDict(strict=True, extra="forbid")
amount_cents: int
currency: str
extra="forbid" also rejects unknown fields, which catches client typos like ammount_cents.
Validating things that aren't models
TypeAdapter validates any type, which is handy for lists or unions at a boundary:
from pydantic import TypeAdapter
ids = TypeAdapter(list[int]).validate_python(["1", 2, "3"]) # [1, 2, 3]
Settings from the environment
The companion package pydantic-settings loads configuration from environment variables and .env files with the same validation:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
request_timeout: float = 10.0
debug: bool = False
settings = Settings() # reads DATABASE_URL, REQUEST_TIMEOUT, DEBUG
A misconfigured deployment now fails loudly at startup instead of halfway through a request.
Validating LLM output
When you ask a model for JSON, validate it like any other untrusted input:
class TicketTriage(BaseModel):
category: Literal["billing", "bug", "how-to", "other"]
priority: int = Field(ge=1, le=4)
summary: str = Field(max_length=280)
try:
triage = TicketTriage.model_validate_json(llm_text)
except ValidationError as e:
triage = retry_with_errors(llm_text, e.errors()) # feed the errors back once, then fall back
TicketTriage.model_json_schema() produces a JSON Schema you can pass to APIs that support structured outputs.
v1 to v2: the renames that trip people up
| Pydantic v1 | Pydantic v2 |
|---|---|
.dict() / .json() |
.model_dump() / .model_dump_json() |
parse_obj() / parse_raw() |
model_validate() / model_validate_json() |
@validator / @root_validator |
@field_validator / @model_validator |
class Config: |
model_config = ConfigDict(...) |
BaseSettings in pydantic |
pydantic-settings package |
Verdict
Put Pydantic at every boundary where data enters your code: HTTP requests, configuration, queues, third-party APIs and LLM responses. It's the cheapest way to turn "weird production bug" into "clear error at the door". If you use FastAPI, you're already halfway there, since it uses Pydantic models for request and response validation.
Written by RecallRun Editors for the RecallRun community. Community posts are checked for safety and reviewed by our editors before publishing, but the views and claims are the author's own. Links are the author's; open them with care. Report this post.
More from the community
- Tools
DuckDB: fast SQL analytics on Parquet, CSV and DataFrames without a server
DuckDB is an in-process analytical database: no server, just a library. Here is how to query files and DataFrames directly, and where it fits next to pandas, Postgres and Spark.
- Tools
Ruff: replacing Flake8, isort and Black with one fast linter and formatter
How to set up Ruff as both linter and formatter, which rule sets are worth enabling, and how to roll it out on an existing codebase without a giant noisy diff.
- Tools
uv: one fast tool for Python packages, virtual environments and versions
uv replaces pip, virtualenv, pip-tools and pyenv-style version management with a single fast binary. Here is how the everyday workflow looks and when it is worth switching.
Share a tech article or a tool you built. Every post is checked and reviewed before it goes live.