Python validation has more options in 2026 than people realize. This post is the working comparison.

The contenders

StrengthsBest for
Pydantic v2Mature, ecosystem, good perfDefault for FastAPI
msgspec10× faster than Pydantic on hot pathHigh-throughput services
attrsPure dataclass replacement; no validationInternal types
dataclassesStdlibInternal types where attrs is overkill
Pydantic v1LegacyOld 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:

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

NeedPick
FastAPI appPydantic v2
LLM structured outputPydantic v2 (or Instructor)
High-throughput Kafka consumermsgspec
Internal value typesdataclasses or attrs
Settings / configpydantic-settings
Existing Pydantic v1Migrate 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

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 .