# cattrs: composable class converters for attrs and dataclasses

> cattrs turns dictionaries into attrs classes and dataclasses and back, validating as it goes, with the conversion rules kept separate from the models and pre-configured converters for the usual serialization libraries. It suits a Python codebase with typed data classes at its boundaries, and its central design choice is that it refuses to guess when a type could be read two ways.

**python-attrs/cattrs** — Composable custom class converters for attrs, dataclasses and friends.

- Repository: https://github.com/python-attrs/cattrs
- Website: https://catt.rs
- Stars: 1,060 · Forks: 192
- Language: Python
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/python-attrs-cattrs

## The two functions, and the promise they make

cattrs is described as a Swiss Army knife for unstructuring and validating data in Python, and in practice that means it converts unstructured dictionaries into proper classes and back, while validating their contents. The tagline is that validation belongs at the edges, which is a statement about where the library should sit in an application. The example shows the whole surface in a few lines:

```python
>>> from attrs import define
>>> from cattrs import structure, unstructure
>>> @define
... class C:
...     a: int
...     b: list[str]
>>> instance = structure({'a': 1, 'b': ['x', 'y']}, C)
>>> instance
C(a=1, b=['x', 'y'])
>>> unstructure(instance)
{'a': 1, 'b': ['x', 'y']}
```

Two functions, `structure` and `unstructure`, and the second one is the round trip back to plain data. The example class is an attrs class with the modern `@define` decorator, and the documentation is clear that cattrs works best with attrs classes and with dataclasses where simple unstructuring works out of the box, even for nested data, without polluting your data model with serialization details. That last clause is the design argument in one sentence. The primary language is Python, the licence is MIT, the homepage is catt.rs, the last push was on 2026-10-08, and the releases are v26.2.1 on 2026-09-26, v26.2.0 on 2026-09-06 and v26.1.0 on 2026-02-18.

## What structuring handles out of the box

The supported type list is long and specific, and it is worth reading as a checklist of what you can put in a class. `typing.Optional[T]` and its 3.10+ form `T | None`. `list[T]`, `typing.List[T]`, `typing.MutableSequence[T]` and `typing.Sequence[T]`, all converting to lists. Tuples, both the `tuple[T, ...]` and the `tuple[X, Y, Z]` variants. Sets, frozensets, and the mutable and immutable typing aliases of each. Dictionaries, with `dict[K, V]` and the `typing.Dict`, `MutableMapping` and `Mapping` forms. `typing.TypedDict`, both ordinary and generic. `typing.NewType`. And PEP 695 type aliases on 3.12 and above. Beyond the typing constructs, attrs classes with simple attributes and the usual `__init__` work, and all attrs classes and dataclasses work if their complex attributes have type metadata. Unions of supported attrs classes work when all the classes have a unique field, and unions of anything work if you provide a disambiguation function for it. The footnote explains that simple attributes are those that can be assigned unstructured data, like numbers, strings and collections of unstructured data.

## Unstructuring, and the pass-through rule

The reverse direction has its own rules, and one of them is the reason cattrs never surprises you. attrs classes and dataclasses are converted into dictionaries in a way similar to `attrs.asdict()`, or into tuples in a way similar to `attrs.astuple()`. Enumeration instances are converted to their values. And other types are let through without conversion, which includes types such as integers, dictionaries, lists and instances of non-attrs classes. That pass-through is a deliberate decision rather than an omission. A converter that guessed would have to decide whether a nested plain dictionary should be recursed into and validated or handed over as is, and the two answers lead to different behaviour on the same input. Letting unknown types through means cattrs never raises on a field it does not understand, and the cost is that a mistyped field can pass silently if it is not part of the type you declared.

## The three design decisions that shape everything

The README states three fundamental design decisions, and they are the most useful part of the document. First, un/structuring rules are separate from the models, which allows models to have a one-to-many relationship with the rules, and lets you create rules for models you do not own and cannot change. The `use_class_methods` strategy is the escape hatch for the opposite case, where the model itself defines how it is structured. Second, invent as little as possible and reuse existing ordinary Python, with the example that cattrs did not have a custom exception type to group exceptions until Python sanctioned the `exceptiongroups` package. The stated side-effect is a good one, that in a lot of cases when you are solving cattrs problems you are actually learning Python instead of learning cattrs. Third, resist the temptation to guess, and if there are two ways of solving a problem, cattrs should refuse to guess and let the user configure it themselves. The README is candid that a foolish consistency is the hobgoblin of little minds, so these decisions can and are sometimes broken, but they have proven a good foundation.

## Batteries included, and the preconf package

cattrs comes with pre-configured converters for a number of serialization libraries, and the list tells you which formats it has been exercised against. That covers JSON in three implementations, the standard library, orjson and UltraJSON, plus msgpack, cbor2, bson, PyYAML, tomlkit and msgspec, with the note that msgspec supports only JSON at this time. These are not the same as a JSON encoder, since the point of a preconf is that it wires up the right hooks for a specific library's hooks and its own way of representing things, and the details live in the `cattrs.preconf` package. The custom converters for anything not covered are registered with `register_structure_hook` and `register_unstructure_hook`, which is the extension point that makes the whole thing composable rather than a fixed set of formats.

## How it is tested and benchmarked

The credits section names the tools, and they say something about the project's standards. cattrs is tested with Hypothesis by David R. MacIver, which is the property-based testing library, and that is the right choice for a library whose job is to handle arbitrary shapes of nested data. Testing a converter with hand-written examples proves the examples work, while testing it with generated structures proves the converter handles the space. It is benchmarked using perf and pytest-benchmark, and the pytest configuration in the project file shows the benchmark options in detail, with the sort by fullname, warmup enabled and five warmup iterations, and grouping by fullname, so the numbers are comparable between runs. The project uses a Justfile rather than a Makefile, and the tooling is grouped into dependency groups for lint, test, docs and bench, with ruff for linting, Hypothesis and pytest for testing, Sphinx with the furo theme and myst-parser for the docs, and pyperf for the benchmarks. The build backend is hatchling with hatch-vcs, and mypy runs in strict mode.

## Where cattrs is the wrong tool

Two limits follow from the design. The first is that a library whose selling point is that it will not guess has to make you guess for it in the cases it cannot resolve, and the documentation names those cases, a union of classes with an overlapping field, or a union of anything without a disambiguation function, is one you solve by writing a function yourself. The second is that the unstructuring pass-through rule means a value whose type is not in the type hints reaches the output unvalidated, so a class with an under-annotated field gets less checking than the class next to it. There is also the ordinary cost of the dependency: attrs 25.4.0 or newer, typing-extensions, and on Python below 3.11 an exceptiongroup backport, plus the requirement of Python 3.10 or newer. None of that is heavy. But if your application already has a working Pydantic setup, or if your data is dictionaries all the way down, cattrs is asking you to adopt a class layer for the sake of the conversion, and that is a larger decision than installing a package.

## Conclusion

Adopt cattrs if you have typed classes at the edges of a Python application and want structured data with validation rather than a pile of dictionaries. Skip it if your data is plain dictionaries throughout, since the whole value is the mapping between dicts and classes. Before you start, note that it requires Python 3.10 or newer and attrs 25.4.0 or newer, and remember that cattrs refuses to guess: when a type could be structured two ways, such as a union of classes with an overlapping field, you have to supply a disambiguation function rather than expect it to work out.

## FAQ

### What does cattrs do?

It converts unstructured dictionaries into proper classes and back, validating their contents as it goes. The two entry points are structure for the inbound direction and unstructure for the outbound one, and it works best with attrs classes and with dataclasses where simple unstructuring works out of the box.

### What is the register_structure_hook and register_unstructure_hook pair for?

They register custom converters for any type, which is how the library is extended to a format or a class it does not know about. The pre-configured converters for JSON, orjson, UltraJSON, msgpack, cbor2, bson, PyYAML, tomlkit and msgspec are built on the same mechanism.

### Why does cattrs refuse to guess?

It is one of the three stated design decisions, alongside keeping the un/structuring rules separate from the models and inventing as little as possible. Where a type could be structured two ways, such as a union of classes with an overlapping field, cattrs leaves it to you to supply a disambiguation function rather than picking one.

### What are cattrs' requirements?

Python 3.10 or newer, attrs 25.4.0 or newer and typing-extensions 4.14.0 or newer, with an exceptiongroup backport on Python below 3.11. The licence is MIT and the documentation is at catt.rs.

## Sources

- [License: MIT](https://github.com/python-attrs/cattrs/blob/main/LICENSE)
- [Project website](https://catt.rs)
- [python-attrs/cattrs on GitHub](https://github.com/python-attrs/cattrs)
- [README](https://github.com/python-attrs/cattrs/blob/main/README.md)
- [Releases](https://github.com/python-attrs/cattrs/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/python-attrs-cattrs
