Comparison of dataclasses, attrs, cattrs, and pydantic (v2). Companion to Python Type Checkers Comparison; the boundary-validation stance below is the practical shape of Parse, Don’t Validate - Alexis King.

The four aren’t peers. dataclasses and attrs are class-definition tools (generate __init__, __repr__, __eq__, etc.). cattrs is a conversion layer that sits on top of either. pydantic bundles definition + validation + conversion into one opinionated package.

Capability matrix

dataclassesattrscattrspydantic (v2)
Dependencystdlibpure-Python, zero depsneeds attrsheavy (pydantic-core, Rust)
Boilerplate generationinit/repr/eq/order/hashsame + evolve, aliases, more knobsn/a (uses your classes)yes, via BaseModel or @pydantic.dataclass
__slots__opt-in (3.10+)default with @definen/aopt-in
Frozen/immutableyesyesn/ayes (frozen=True)
Runtime type validationno (__post_init__ only)opt-in per fieldat structure time onlyalways on
Type coercionnoopt-in convertersyes, when structuringyes by default (lax), strict mode available
To dict/JSONasdict (dumb, recursive)asdict (dumb)unstructure, customizablemodel_dump, model_dump_json
From dict/JSONnonostructure, generics/unions/nestedmodel_validate, model_validate_json
JSON Schemanonono (third-party only)built-in
Custom validatorsmanualattrs.validatorshooks per type@field_validator, @model_validator
Works with plain dataclasses / TypedDict / NamedTupleyespartially (TypeAdapter)
Instantiation costvery cheapvery cheapn/anoticeably higher (validation runs every time)
Parsing JSON costgoodvery good (Rust)
Ecosystem pullnone needednone neededpreconfigured converters (orjson, msgspec…)FastAPI, pydantic-settings, most LLM libs

Pros and cons

  • dataclasses: free, everyone knows it, no validation, no parsing. Fine until you need to load data from outside.
  • attrs: strictly more capable and slightly faster than dataclasses, cleaner defaults (@define), but it’s a dependency for a small marginal gain unless you use its validators/converters.
  • cattrs: keeps domain classes plain and puts (de)serialization in a separate, composable converter; excellent error reporting; but it’s a second library to learn and less discoverable than pydantic.
  • pydantic: one-stop shop, great DX, JSON Schema for free, FastAPI-native. Cost: validation on every construction whether you want it or not, a heavy dependency, and it nudges you into putting BaseModel everywhere.

When to use what

  • Internal data carriers, config objects, ASTs, results → dataclasses (or attrs if it’s already in the project).
  • Need validators/converters on internal types but not a serialization framework → attrs.
  • Data crosses a boundary (JSON, YAML, DB rows) and domain classes should stay dependency-free → attrs/dataclasses + cattrs.
  • Building an HTTP API, need JSON Schema/OpenAPI, or codebase is already pydantic-shaped → pydantic.
  • Hot-path serialization where speed dominates → msgspec beats all four on parsing and is stdlib-light.

The expert stance

Validate at the edges, not in the middle. The common failure mode is pydantic models threaded through the whole application: validation cost on every internal construction and a core coupled to a framework. The expert pattern is plain dataclasses/attrs in the core with one conversion layer at the boundary — cattrs if it should be decoupled, pydantic if in a FastAPI world or schema generation matters. Hynek Schlawack (attrs’ author) would say attrs + cattrs; someone shipping a web service on a deadline would say pydantic. Both are right; the mistake is not picking a boundary.