pydantic review
Pydantic turns Python type annotations into runtime parsing and validation rules. A BaseModel accepts dictionaries, JSON, or object attributes, returns typed fields, reports errors with nested locations, and emits JSON Schema for the same contract. Version 2.13.4 is a small patch that preserves RootModel core metadata and adjusts packaging for its pydantic-core dependency on macOS. The current API is the v2 family: model_validate, model_dump, field_validator, and ConfigDict. Old examples built around parse_obj, dict, or validator describe the compatibility layer, not the preferred interface.
Pydantic is a strong default at Python data boundaries, especially when a framework already speaks its models. Learn the v2 contract and make coercion, mutation, and serialization choices explicit before those defaults become API behavior.
We installed it
| Install | ✓ · 0.4s | 5 packages on disk · 8 MB |
| Import | ✓ | import pydantic in 0.19s · pure Python · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pydantic install cleanly?
Yes. In a fresh container with an empty cache, pip install pydantic finished in 0.4s, leaving 5 packages and 8 MB on disk. pip-audit reported no known vulnerabilities.
What does pydantic need to run?
Python >=3.9, and nothing compiled: it is pure Python. In our run import pydantic succeeded in 0.19s, and the package ships py.typed for type checkers.
pydantic or msgspec: which should you use?
msgspec: Use it for tightly scoped typed serialization where throughput and low overhead lead the decision. Pydantic is a strong default at Python data boundaries, especially when a framework already speaks its models.
When should you not use pydantic?
Use dataclasses or attrs when objects are constructed only by trusted Python code and runtime parsing adds no value
Discussed on
- hnPEP 563, PEP 649 and the future of pydantic and FastAPI282 points
- hnBuilding your own CLI coding agent with Pydantic-AI197 points
- hnRun structured extraction on documents/images locally with Ollama and Pydantic170 points
- hnApproximating sum types in Python with Pydantic158 points
- hnShow HN: Oxyde – Pydantic-native async ORM with a Rust core155 points
Use it if
- You receive JSON, environment values, webhook bodies, or tool output and need typed values plus structured validation errors
- Your framework already consumes Pydantic models for request parsing, response schemas, or generated API documentation
- You need JSON Schema from the same definitions used by Python code, including aliases, unions, constraints, and nested models
- You want coercion for ordinary boundary data but need the option to turn on strict parsing globally or for individual fields
- Use dataclasses or attrs when objects are constructed only by trusted Python code and runtime parsing adds no value
- Use msgspec when a narrow, high-volume JSON or MessagePack path matters more than Pydantic's validators and schema customization
- Do not start a v2 codebase from old tutorials: BaseSettings moved to pydantic-settings, validators changed, and serialization methods were renamed
- Avoid default coercion when strings such as '1' must never become integers or booleans; enable strict mode explicitly or choose a stricter boundary
- Do not expect assignment to be rechecked after construction unless validate_assignment is enabled in model configuration
Setup reality
Our fresh Python 3.12 install of Pydantic 2.13.4 finished in 0.4 seconds. It produced 5 packages using 8 MB on disk, and pip-audit reported 0 known vulnerabilities. Pydantic declares 6 direct dependencies, requires Python 3.9 or newer, is packaged as pure Python, and ships py.typed. The import test completed in 0.19 seconds. Its installed dependency set includes the separate validation engine used by v2.
Start with the v2 method names. Use model_validate for Python input, model_validate_json for JSON text or bytes, and model_dump or model_dump_json for output. Pydantic normally converts compatible input, including numeric strings. ConfigDict(strict=True) changes that contract. Decide this at the boundary rather than letting different callers assume different coercion rules.
Settings and several specialized types live outside the base package. Environment-backed models require pydantic-settings; email validation commonly uses the email extra. Model construction can also perform network-free schema work at import time, so build reusable TypeAdapter instances once instead of creating them inside a request loop. Forward references may need model_rebuild after all referenced classes exist.
Validation protects model creation, not every later mutation. Enable validate_assignment if fields will be changed and must remain valid. model_construct skips validation and should be reserved for already trusted data. Serialization has its own rules for aliases, excluded values, computed fields, and subclasses, so test the exact model_dump options used at an API or persistence boundary.
Patterns
Parse input into a model define-model
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
tags: list[str] = []
user = User.model_validate({"id": "42", "name": "Ada"})Default validation converts compatible values, so the string ID becomes an integer. Enable strict mode when that is undesirable.
Return structured validation errors read-validation-errors
from pydantic import ValidationError
try:
User.model_validate({"id": "wrong", "name": 7})
except ValidationError as exc:
for error in exc.errors():
print(error["loc"], error["type"], error["msg"])The error list is suitable for API responses, but review whether the included input values could expose secrets.
Normalize and validate one field add-field-validator
from pydantic import BaseModel, field_validator
class Account(BaseModel):
handle: str
@field_validator("handle")
@classmethod
def normalize_handle(cls, value: str) -> str:
value = value.strip().lower()
if " " in value:
raise ValueError("spaces are not allowed")
return valueA validator must return the accepted value. ValueError is converted into a field-scoped ValidationError entry.
Check a relationship after field parsing validate-related-fields
from typing_extensions import Self
from pydantic import BaseModel, model_validator
class Window(BaseModel):
start: int
end: int
@model_validator(mode="after")
def ordered(self) -> Self:
if self.start > self.end:
raise ValueError("start must be at most end")
return selfAn after validator receives the model instance. A before validator receives raw input and must handle shapes other than a dictionary.
Reject implicit conversions enable-strict-mode
from pydantic import BaseModel, ConfigDict
class Event(BaseModel):
model_config = ConfigDict(strict=True)
count: int
Event.model_validate({"count": 3})With strict mode enabled, a value such as the string '3' fails instead of being converted.
Serialize with aliases and exclusions serialize-model
payload = user.model_dump(
mode="json",
by_alias=True,
exclude_none=True,
)
text = user.model_dump_json(by_alias=True, exclude_none=True)model_dump_json returns a string. Use mode='json' when the dictionary itself must contain JSON-compatible values.
Reuse a TypeAdapter for a non-model type validate-arbitrary-type
from pydantic import TypeAdapter
rows_adapter = TypeAdapter(list[dict[str, int]])
rows = rows_adapter.validate_json('[{"count": "3"}]')
schema = rows_adapter.json_schema()Create the adapter once and reuse it; rebuilding validators inside a hot function wastes schema construction work.
Select a tagged union branch discriminate-union
from typing import Annotated, Literal
from pydantic import BaseModel, Field
class Email(BaseModel):
kind: Literal["email"]
address: str
class Sms(BaseModel):
kind: Literal["sms"]
number: str
Channel = Annotated[Email | Sms, Field(discriminator="kind")]The discriminator avoids trying every union member and gives errors tied to the selected branch.
Recheck values when fields change validate-assignment
from pydantic import BaseModel, ConfigDict, Field
class Job(BaseModel):
model_config = ConfigDict(validate_assignment=True)
retries: int = Field(ge=0, le=10)
job = Job(retries=2)
job.retries = 3Without validate_assignment, later writes bypass field validation even though initial construction was checked.
Generate JSON Schema for a model emit-json-schema
schema = User.model_json_schema(by_alias=True)
print(schema["properties"])Schema customization and runtime validation are related but separate; test the generated document consumed by OpenAPI or another tool.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| msgspec | PyPI | Use it for tightly scoped typed serialization where throughput and low overhead lead the decision. |
| attrs | PyPI | Use it to define ergonomic Python classes without treating external input as a validation problem. |
| marshmallow | PyPI | Use it when serialization schemas should remain separate from domain classes. |
More utils guides
lru-cache · type-fest · ajv · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

