cattrs review
cattrs 26.1.0 converts dictionaries and other plain Python values into typed attrs classes, dataclasses, TypedDicts, named tuples, collections, and unions, then turns those objects back into values a serializer can handle. Conversion policy belongs to a `Converter`, which lets one class use different field names or formats at separate boundaries. The current release adds a `tomllib` preset and lets `Annotated[T, override(...)]` alter generated hooks per field. It requires Python 3.10 or newer.
cattrs 26.1.0 took 0.2 seconds and 1 MB in our sandbox, with typed pure-Python code and no audit findings, making it a low-cost addition for apps that already own typed models. Install Pydantic instead when validation rules and schema generation are the main requirement.
We installed it
| Install | ✓ · 0.2s | 3 packages on disk · 1 MB |
| Import | ✓ | import cattr in 0.18s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does cattrs install cleanly?
Yes. In a fresh container with an empty cache, pip install cattrs finished in 0.2s, leaving 3 packages and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does cattrs need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import cattr succeeded in 0.18s, and the package ships py.typed for type checkers.
cattrs or dacite: which should you use?
dacite: Choose it for dictionary-to-dataclass construction when serialization back to plain values is outside the job. cattrs 26.1.0 took 0.2 seconds and 1 MB in our sandbox, with typed pure-Python code and no audit findings, making it a low-cost addition for apps that already own typed models.
When should you not use cattrs?
Use Pydantic if the model layer must own runtime constraints, settings, JSON Schema, and OpenAPI generation. cattrs concentrates on conversion according to type information and registered hooks.
Use it if
- You already model data with attrs classes or dataclasses and need nested input converted without putting parsing methods on the models.
- The same class must use one representation in an HTTP API and another in a cache, queue, or file.
- Your boundary contains typed containers, generics, aliases, defaults, or unions that need explicit conversion rules.
- You want ready-made converters for stdlib JSON, orjson, ujson, msgpack, cbor2, BSON, YAML, tomlkit, or tomllib.
- Use Pydantic if the model layer must own runtime constraints, settings, JSON Schema, and OpenAPI generation. cattrs concentrates on conversion according to type information and registered hooks.
- Walk away if union members have indistinguishable input shapes and you cannot add a tag or write a disambiguation function. cattrs deliberately refuses to guess the target class.
- A plain `Converter` does not decide wire encodings for every datetime, Decimal, UUID, or application identifier. Those types need hooks or the matching format preset.
- Release 26.1.0 cannot run on Python 3.9. A fleet that still includes 3.9 needs an older cattrs release or a runtime upgrade.
- Avoid the module-level converter in an application where plugins register hooks. One plugin can change the conversion behavior observed by every later global call.
Setup reality
Our install of cattrs 26.1.0 finished in 0.2 seconds inside a fresh Python 3.12 Bookworm container. The environment ended with 3 packages and 1 MB on disk. pip-audit found 0 known vulnerabilities. The distribution is pure Python, requires Python 3.10+, includes py.typed, and carries the MIT license. Importing the older compatibility module name, cattr, worked in 0.18 seconds.
The convenience functions use one process-wide converter. Registering a hook on that object changes later calls anywhere in the process, so services with multiple payload formats should construct separate Converter instances. Decide how to treat extra keys, aliases, omitted defaults, and detailed errors before the first request reaches the boundary. Import order is a poor configuration system.
Version 26.1.0 understands supported annotated containers and class forms, but it does not invent a transport representation for every scalar. Register both structure and unstructure hooks for domain values, or start with a preset whose serializer matches the wire format. The new tomllib preset reads TOML using the standard library. It cannot write TOML because tomllib itself has no dump function.
Generated hooks are cached after cattrs first sees a type. Install hook factories and union strategies before workers process that type. An untagged union works only when the members can be distinguished from required fields; otherwise add a tag or a discriminator. Field metadata supplied through Annotated[T, override(...)] is useful for a single representation, while separate converters remain easier to reason about when one model crosses several boundaries.
Patterns
Convert nested values in both directions structure-and-unstructure
from attrs import define
from cattrs import structure, unstructure
@define
class User:
id: int
tags: list[str]
u = structure({"id": 1, "tags": ["a", "b"]}, User)
print(u) # User(id=1, tags=['a', 'b'])
print(unstructure(u)) # {'id': 1, 'tags': ['a', 'b']}These helpers share the global converter. Use your own `Converter` once the application needs custom policy or plugin isolation.
Set boundary policy on one converter own-converter
from cattrs import Converter
converter = Converter(forbid_extra_keys=True, omit_if_default=True)
# forbid_extra_keys: unknown dict keys raise ForbiddenExtraKeysError
# omit_if_default: fields equal to their default are left out of output`forbid_extra_keys` rejects unexpected mapping keys, while `omit_if_default` affects output. The generated converter caches functions per type.
Teach a converter an ISO datetime datetime-hooks
from datetime import datetime, timezone
from cattrs import Converter
converter = Converter()
converter.register_unstructure_hook(datetime, lambda dt: dt.isoformat())
converter.register_structure_hook(
datetime, lambda v, _: datetime.fromisoformat(v)
)
converter.structure("2026-08-06T10:00:00+00:00", datetime)The structure callback receives the input plus its requested type. The reverse callback receives the Python object, so both directions need registration.
Give API fields different names rename-fields
from attrs import define
from cattrs import Converter, override
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn
@define
class Account:
account_id: int
display_name: str
c = Converter()
renames = {
"account_id": override(rename="accountId"),
"display_name": override(rename="displayName"),
}
c.register_unstructure_hook(Account, make_dict_unstructure_fn(Account, c, **renames))
c.register_structure_hook(Account, make_dict_structure_fn(Account, c, **renames))
c.unstructure(Account(1, "Ann")) # {'accountId': 1, 'displayName': 'Ann'}Install a generated hook for input and another for output. Registering only one direction produces a mapping that cannot round-trip.
Serialize through the orjson preset preconf-orjson
# pip install 'cattrs[orjson]'
from attrs import define
from cattrs.preconf.orjson import make_converter
@define
class Event:
name: str
c = make_converter()
raw = c.dumps(Event("deploy")) # b'{"name":"deploy"}'
print(c.loads(raw, Event))The `orjson` extra supplies the serializer dependency. Presets add `dumps` and `loads` and include format-specific handling for common scalar values.
Discriminate a union with a tag tagged-union
from attrs import define
from cattrs import Converter
from cattrs.strategies import configure_tagged_union
@define
class Cat:
lives: int
@define
class Dog:
breed: str
c = Converter()
configure_tagged_union(Cat | Dog, c, tag_name="kind")
c.structure({"kind": "Cat", "lives": 9}, Cat | Dog)Default tag values are class names. Set `tag_generator` when wire values must stay independent of Python class names.
Turn grouped failures into field paths readable-errors
from attrs import define
from cattrs import Converter, transform_error
from cattrs.errors import ClassValidationError
@define
class Point:
x: int
y: int
c = Converter()
try:
c.structure({"x": "nope"}, Point)
except ClassValidationError as exc:
for line in transform_error(exc):
print(line)Detailed validation can collect several nested failures in an `ExceptionGroup`; `transform_error` renders paths such as `@ $.x`.
Keep a secret out of output omit-and-rename-one-field
from attrs import define, field
from cattrs import Converter, override
from cattrs.gen import make_dict_unstructure_fn
@define
class Session:
user: str
token: str = field(default="")
c = Converter()
c.register_unstructure_hook(
Session, make_dict_unstructure_fn(Session, c, token=override(omit=True))
)
c.unstructure(Session("ann", "secret")) # {'user': 'ann'}This generated hook changes unstructuring only. Input policy must be configured separately if the same field should also be rejected or renamed.
Apply one output rule to attrs classes hook-factory-all-classes
from attrs import has
from cattrs import Converter
from cattrs.gen import make_dict_unstructure_fn
c = Converter()
c.register_unstructure_hook_factory(
has,
lambda cls, conv: make_dict_unstructure_fn(
cls, conv, _cattrs_omit_if_default=True
),
)A hook factory runs the first time each matching class is used, then its result is cached. Register factories before converting those classes.
Recognize subclasses through a base type include-subclasses
from attrs import define
from cattrs import Converter
from cattrs.strategies import include_subclasses
@define
class Shape:
name: str
@define
class Circle(Shape):
radius: float
c = Converter()
include_subclasses(Shape, c)
c.structure({"name": "c", "radius": 1.0}, Shape) # Circle(...)`include_subclasses` sees classes already imported into `__subclasses__()`. Import implementations first and tag members whose fields overlap.
Load a dataclass and a TypedDict dataclasses-and-typeddict
from dataclasses import dataclass
from typing import TypedDict
from cattrs import Converter
@dataclass
class Item:
sku: str
qty: int
class Row(TypedDict):
sku: str
qty: int
c = Converter()
c.structure({"sku": "a", "qty": 2}, Item)
c.structure({"sku": "a", "qty": 2}, Row)Dataclasses and TypedDicts use the same converter machinery as attrs models; an attrs base class is not required at the data boundary.
Raise the first conversion error disable-detailed-validation
from cattrs import Converter
fast = Converter(detailed_validation=False)
# raises the first underlying error (ValueError, KeyError, ...)
# instead of collecting an ExceptionGroupWith detailed validation disabled, cattrs stops at the first underlying exception and omits the grouped field-path report. Use it only when that diagnostic loss is acceptable.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| dacite | PyPI | Choose it for dictionary-to-dataclass construction when serialization back to plain values is outside the job. |
| mashumaro | PyPI | Choose it when generated methods attached to models fit better than conversion policy held in separate objects. |
| pydantic | PyPI | Choose it when validation constraints, schemas, settings, and framework support should come with the model API. |
More utils guides
lru-cache · ajv · type-fest · 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.

