Python validation has more options in 2026 than people realize. This post is the working comparison.
The contenders
| Strengths | Best for | |
|---|---|---|
| Pydantic v2 | Mature, ecosystem, good perf | Default for FastAPI |
| msgspec | 10× faster than Pydantic on hot path | High-throughput services |
| attrs | Pure dataclass replacement; no validation | Internal types |
| dataclasses | Stdlib | Internal types where attrs is overkill |
| Pydantic v1 | Legacy | Old codebases |
Pydantic v2
The default. Mature. Wide library support. See Pydantic v2 Deep Dive .
from pydantic import BaseModel, Field, EmailStr
class User(BaseModel):
email: EmailStr
full_name: str = Field(..., min_length=1, max_length=120)
age: int | None = None
Best when:
- API I/O (FastAPI auto-uses it).
- LLM structured output (Structured Output for LLMs ).
- Settings / config.
msgspec
C-implemented; focused; faster.
import msgspec
class User(msgspec.Struct):
email: str
full_name: str
age: int | None = None
# Decode + validate from JSON bytes in one shot
user = msgspec.json.decode(raw_bytes, type=User)
Best when:
- Decoding huge volumes of messages (Kafka, queues).
- API endpoints serving 100k+ req/sec.
- Memory matters.
Tradeoffs: smaller ecosystem; less feature surface (no model_validator-style hooks the same way).
attrs
from attrs import define, field
@define
class Order:
id: int
total: float = field(validator=lambda i, a, v: v > 0)
Pure dataclass replacement with attrs’s nicer ergonomics. Some validation via validators. Not for API I/O — for internal types where you want clean classes.
stdlib dataclasses
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
For value types that don’t need validation. Frozen + slots gives you immutable + memory-efficient.
Decision matrix
| Need | Pick |
|---|---|
| FastAPI app | Pydantic v2 |
| LLM structured output | Pydantic v2 (or Instructor) |
| High-throughput Kafka consumer | msgspec |
| Internal value types | dataclasses or attrs |
| Settings / config | pydantic-settings |
| Existing Pydantic v1 | Migrate to v2 |
Performance
Decoding 100k JSON objects:
- json + manual: 1.0× (baseline).
- Pydantic v2: 1.2× slower (validation overhead).
- msgspec: 0.5× (faster than json + manual).
- dataclasses + manual decode: 1.0×.
For a FastAPI app where DB dominates: Pydantic is fine. For a Kafka consumer parsing 1M msg/sec: msgspec wins decisively.
Migration: Pydantic v1 → v2
# v1
class Config:
orm_mode = True
@validator("name")
def name_must_be_long(cls, v):
...
# v2
model_config = ConfigDict(from_attributes=True)
@field_validator("name")
@classmethod
def name_must_be_long(cls, v):
...
Codemod available: bump-pydantic. Not perfect; review.
Hybrid
Use both:
# API boundary: Pydantic for ergonomics + ecosystem
class UserOut(BaseModel):
id: int
email: str
# Hot path: msgspec for raw decode
class EventDecoded(msgspec.Struct):
user_id: int
action: str
ts: int
Each at the layer it shines.
Common mistakes
1. Pydantic for high-throughput message decoding
Validation overhead per message × 1M msg/sec = wasted compute. msgspec or hand-decode.
2. msgspec at API boundary
Lose ecosystem (FastAPI, OpenAPI generation, ORM integration). Stick with Pydantic.
3. Dataclasses for API I/O
No validation. Bad inputs propagate. Use Pydantic / msgspec.
4. Mixing v1 and v2 Pydantic
Compat shims work but produce deprecation warnings; clean up gradually.
Read this next
- Pydantic v2 Deep Dive
- FastAPI + Pydantic v2 + SQLAlchemy 2.0
- Modern Python Tooling 2026
- Modern Python Type Hints 2026
If you want benchmarks on your specific shapes, the harness is at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .