Built-in Hooks#

cattrs converters come with with a large repertoire of un/structuring hooks built-in. As always, complex hooks compose with simpler ones.

Primitive Values#

int, float, str, bytes#

When structuring, use any of these types to coerce the object to that type.

>>> cattrs.structure(1, str)
'1'
>>> cattrs.structure("1", float)
1.0

In case the conversion isn’t possible the expected exceptions will be propagated out. The particular exceptions are the same as if you’d tried to do the coercion directly.

>>> cattrs.structure("not-an-int", int)
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: 'not-an-int'

Coercion is performed for performance and compatibility reasons. Any of these hooks can be overriden if pure validation is required instead.

>>> c = Converter()

>>> @c.register_structure_hook
... def validate(value, type) -> int:
...     if not isinstance(value, type):
...         raise ValueError(f'{value!r} not an instance of {type}')

>>> c.structure("1", int)
Traceback (most recent call last):
...
ValueError: '1' not an instance of <class 'int'>

When unstructuring, these types are passed through unchanged.

Enums#

Enums are structured by their values, and unstructured to their values. This works even for complex values, like tuples.

>>> @unique
... class CatBreed(Enum):
...    SIAMESE = "siamese"
...    MAINE_COON = "maine_coon"
...    SACRED_BIRMAN = "birman"

>>> cattrs.structure("siamese", CatBreed)
<CatBreed.SIAMESE: 'siamese'>

>>> cattrs.unstructure(CatBreed.SIAMESE)
'siamese'

Again, in case of errors, the expected exceptions are raised.

pathlib.Path#

pathlib.Path objects are structured using their string value, and unstructured into their string value.

>>> from pathlib import Path

>>> cattrs.structure("/root", Path)
PosixPath('/root')

>>> cattrs.unstructure(Path("/root"))
'/root'

In case the conversion isn’t possible, the resulting exception is propagated out.

New in version 23.1.0.

attrs Classes and Dataclasses#

attrs classes and dataclasses work out of the box. The fields require type annotations (even if static type-checking is not being used), or they will be treated as typing.Any.

When structuring, given a mapping d and class A, cattrs will instantiate A with d unpacked.

>>> @define
... class A:
...     a: int
...     b: int

>>> cattrs.structure({'a': 1, 'b': '2'}, A)
A(a=1, b=2)

Tuples can be structured into classes using structure_attrs_fromtuple() (fromtuple as in the opposite of attrs.astuple and BaseConverter.unstructure_attrs_astuple()).

>>> @define
... class A:
...     a: str
...     b: int

>>> cattrs.structure_attrs_fromtuple(['string', '2'], A)
A(a='string', b=2)

Loading from tuples can be made the default by creating a new Converter with unstruct_strat=cattr.UnstructureStrategy.AS_TUPLE.

>>> converter = cattrs.Converter(unstruct_strat=cattr.UnstructureStrategy.AS_TUPLE)
>>> @define
... class A:
...     a: str
...     b: int

>>> converter.structure(['string', '2'], A)
A(a='string', b=2)

Structuring from tuples can also be made the default for specific classes only by registering a hook the usual way.

>>> converter = cattrs.Converter()

>>> @define
... class A:
...     a: str
...     b: int

>>> converter.register_structure_hook(A, converter.structure_attrs_fromtuple)

Generics#

Generic attrs classes and dataclasses are fully supported, both using typing.Generic and PEP 695.

>>> @define
... class A[T]:
...    a: T

>>> cattrs.structure({"a": "1"}, A[int])
A(a=1)

Using Attribute Types and Converters#

By default, structure() will use hooks registered using register_structure_hook() to convert values to the attribute type, and proceed to invoking any converters registered on attributes with field.

>>> from ipaddress import IPv4Address, ip_address
>>> converter = cattrs.Converter()

# Note: register_structure_hook has not been called, so this will fallback to 'ip_address'
>>> @define
... class A:
...     a: IPv4Address = field(converter=ip_address)

>>> converter.structure({'a': '127.0.0.1'}, A)
A(a=IPv4Address('127.0.0.1'))

Priority is still given to hooks registered with register_structure_hook(), but this priority can be inverted by setting prefer_attrib_converters to True.

>>> converter = cattrs.Converter(prefer_attrib_converters=True)

>>> @define
... class A:
...     a: int = field(converter=lambda v: int(v) + 5)

>>> converter.structure({'a': '10'}, A)
A(a=15)

See also

If an attrs or dataclass class uses inheritance and as such has one or several subclasses, it can be structured automatically to its exact subtype by using the include subclasses strategy.

Unions#

Unions of NoneType and a single other type (also known as optionals) are supported by a special case.

Automatic Disambiguation#

cattrs includes an opinionated strategy for automatically handling unions of attrs classes; see Default Union Strategy for details.

When unstructuring these kinds of unions, each union member will be unstructured according to the hook for that type.

Unions of Simple Types#

cattrs comes with the Union Passthrough, which enables converters to structure unions of many primitive types and literals. This strategy can be applied to any converter, and is pre-applied to all preconf converters according to their underlying protocols.

Special Typing Forms#

typing.Any#

When structuring, use typing.Any to avoid applying any conversions to the object you’re structuring; it will simply be passed through.

>>> cattrs.structure(1, Any)
1
>>> d = {1: 1}
>>> cattrs.structure(d, Any) is d
True

When unstructuring, typing.Any will make the value be unstructured according to its runtime class.

Changed in version 24.1.0: Previously, the unstructuring rules for Any were underspecified, leading to inconsistent behavior.

Changed in version 24.1.0: typing_extensions.Any is now also supported.

typing.Literal#

When structuring, PEP 586 literals are validated to be in the allowed set of values.

>>> from typing import Literal

>>> cattrs.structure(1, Literal[1, 2])
1

When unstructuring, literals are passed through.

New in version 1.7.0.

typing.NamedTuple#

Named tuples with type hints (created from typing.NamedTuple) are supported.

New in version 24.1.0.

typing.Final#

PEP 591 Final attribute types (Final[int]) are supported and handled according to the inner type (in this case, int).

New in version 23.1.0.

typing.Annotated#

PEP 593 annotations (typing.Annotated[type, ...]) are supported and are handled using the first type present in the annotated type.

New in version 1.4.0.

Type Aliases#

Type aliases are supported on Python 3.12+ and are handled according to the rules for their underlying type. Their hooks can also be overriden using Predicate Hooks.

Warning

Type aliases using typing.TypeAlias aren’t supported since there is no way at runtime to distinguish them from their underlying types.

>>> from datetime import datetime, UTC

>>> type IsoDate = datetime

>>> converter = cattrs.Converter()
>>> converter.register_structure_hook_func(
...     lambda t: t is IsoDate, lambda v, _: datetime.fromisoformat(v)
... )
>>> converter.register_unstructure_hook_func(
...     lambda t: t is IsoDate, lambda v: v.isoformat()
... )

>>> converter.structure("2022-01-01", IsoDate)
datetime.datetime(2022, 1, 1, 0, 0)
>>> converter.unstructure(datetime.now(UTC), unstructure_as=IsoDate)
'2023-11-20T23:10:46.728394+00:00'

New in version 24.1.0.

typing.NewType#

NewTypes are supported and are handled according to the rules for their underlying type. Their hooks can also be overriden using Converter.register_structure_hook().

>>> from typing import NewType
>>> from datetime import datetime

>>> IsoDate = NewType("IsoDate", datetime)

>>> converter = cattrs.Converter()
>>> converter.register_structure_hook(IsoDate, lambda v, _: datetime.fromisoformat(v))

>>> converter.structure("2022-01-01", IsoDate)
datetime.datetime(2022, 1, 1, 0, 0)

New in version 22.2.0.

typing.Protocol#

Protocols cannot be structured by default and so require custom hooks.

Protocols are unstructured according to the actual runtime type of the value.

New in version 1.9.0.