Customizing (Un-)structuring#
This section describes customizing the unstructuring and structuring processes in cattrs.
Custom (Un-)structuring Hooks#
You can write your own structuring and unstructuring functions and register them for types using Converter.register_structure_hook()
and Converter.register_unstructure_hook()
.
This approach is the most flexible but also requires the most amount of boilerplate.
register_structure_hook()
and register_unstructure_hook()
use a Python singledispatch under the hood.
singledispatch is powerful and fast but comes with some limitations; namely that it performs checks using issubclass()
which doesn’t work with many Python types.
Some examples of this are:
various generic collections (
list[int]
is not a subclass oflist
)literals (
Literal[1]
is not a subclass ofLiteral[1]
)generics (
MyClass[int]
is not a subclass ofMyClass
)protocols, unless they are
runtime_checkable
various modifiers, such as
Final
andNotRequired
newtypes and 3.12 type aliases
typing.Annotated
… and many others. In these cases, predicate functions should be used instead.
Use as Decorators#
register_structure_hook()
and register_unstructure_hook()
can also be used as decorators.
When used this way they behave a little differently.
register_structure_hook()
will inspect the return type of the hook and register the hook for that type.
@converter.register_structure_hook
def my_int_hook(val: Any, _) -> int:
"""This hook will be registered for `int`s."""
return int(val)
register_unstructure_hook()
will inspect the type of the first argument and register the hook for that type.
from datetime import datetime
@converter.register_unstructure_hook
def my_datetime_hook(val: datetime) -> str:
"""This hook will be registered for `datetime`s."""
return val.isoformat()
The non-decorator approach is still recommended when dealing with lambdas, hooks produced elsewhere, unannotated hooks and situations where type introspection doesn’t work.
New in version 24.1.0.
Predicate Hooks#
A predicate is a function that takes a type and returns true or false depending on whether the associated hook can handle the given type.
The register_unstructure_hook_func()
and register_structure_hook_func()
are used
to link un/structuring hooks to arbitrary types. These hooks are then called predicate hooks, and are very powerful.
Predicate hooks are evaluated after the singledispatch hooks. In the case where both a singledispatch hook and a predicate hook are present, the singledispatch hook will be used. Predicate hooks are checked in reverse order of registration, one-by-one, until a match is found.
The following example demonstrates a predicate that checks for the presence of an attribute on a class (custom
), and then overrides the structuring logic.
>>> class D:
... custom = True
... def __init__(self, a):
... self.a = a
... def __repr__(self):
... return f'D(a={self.a})'
... @classmethod
... def deserialize(cls, data):
... return cls(data["a"])
>>> cattrs.register_structure_hook_func(
... lambda cls: getattr(cls, "custom", False), lambda d, t: t.deserialize(d)
... )
>>> cattrs.structure({'a': 2}, D)
D(a=2)
Hook Factories#
Hook factories are higher-order predicate hooks: they are functions that produce hooks. Hook factories are commonly used to create very optimized hooks by offloading part of the work into a separate, earlier step.
Hook factories are registered using Converter.register_unstructure_hook_factory()
and Converter.register_structure_hook_factory()
.
Here’s an example showing how to use hook factories to apply the forbid_extra_keys
to all attrs classes:
>>> from attrs import define, has
>>> from cattrs import Converter
>>> from cattrs.gen import make_dict_structure_fn
>>> c = Converter()
>>> c.register_structure_hook_factory(
... has,
... lambda cl: make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True)
... )
>>> @define
... class E:
... an_int: int
>>> c.structure({"an_int": 1, "else": 2}, E)
Traceback (most recent call last):
...
cattrs.errors.ForbiddenExtraKeysError: Extra fields in constructor for E: else
Hook factories can receive the current converter by exposing an additional required parameter.
A complex use case for hook factories is described over at Using Factory Hooks.
Use as Decorators#
register_unstructure_hook_factory()
and
register_structure_hook_factory()
can also be used as decorators.
Here’s an example of using an unstructure hook factory to handle unstructuring queues.
>>> from queue import Queue
>>> from typing import get_origin
>>> from cattrs import Converter
>>> c = Converter()
>>> @c.register_unstructure_hook_factory(lambda t: get_origin(t) is Queue)
... def queue_hook_factory(cl: Any, converter: Converter) -> Callable:
... type_arg = get_args(cl)[0]
... elem_handler = converter.get_unstructure_hook(type_arg)
...
... def unstructure_hook(v: Queue) -> list:
... res = []
... while not v.empty():
... res.append(elem_handler(v.get_nowait()))
... return res
...
... return unstructure_hook
>>> q = Queue()
>>> q.put(1)
>>> q.put(2)
>>> c.unstructure(q, unstructure_as=Queue[int])
[1, 2]
Customizing Collections#
The cattrs.cols
module contains predicates and hook factories useful for customizing collection handling.
These hook factories can be wrapped to apply complex customizations.
Available predicates are:
Tip
These predicates aren’t cattrs-specific and may be useful in other contexts.
>>> from cattrs.cols import is_sequence
>>> is_sequence(list[str])
True
Available hook factories are:
Additional predicates and hook factories will be added as requested.
For example, by default sequences are structured from any iterable into lists. This may be too lax, and additional validation may be applied by wrapping the default list structuring hook factory.
from cattrs.cols import is_sequence, list_structure_factory
c = Converter()
@c.register_structure_hook_factory(is_sequence)
def strict_list_hook_factory(type, converter):
# First, we generate the default hook...
list_hook = list_structure_factory(type, converter)
# Then, we wrap it with a function of our own...
def strict_list_hook(value, type):
if not isinstance(value, list):
raise ValueError("Not a list!")
return list_hook(value, type)
# And finally, we return our own composite hook.
return strict_list_hook
Now, all sequence structuring will be stricter:
>>> c.structure({"a", "b", "c"}, list[str])
Traceback (most recent call last):
...
ValueError: Not a list!
New in version 24.1.0.
Customizing Named Tuples#
Named tuples can be un/structured using dictionaries using the namedtuple_dict_structure_factory
and namedtuple_dict_unstructure_factory
hook factories.
To unstructure all named tuples into dictionaries:
>>> from typing import NamedTuple
>>> from cattrs.cols import is_namedtuple, namedtuple_dict_unstructure_factory
>>> c = Converter()
>>> c.register_unstructure_hook_factory(is_namedtuple, namedtuple_dict_unstructure_factory)
<function namedtuple_dict_unstructure_factory at ...>
>>> class MyNamedTuple(NamedTuple):
... a: int
>>> c.unstructure(MyNamedTuple(1))
{'a': 1}
To only un/structure some named tuples into dictionaries, change the predicate function when registering the hook factory:
>>> c.register_unstructure_hook_factory(
... lambda t: t is MyNamedTuple,
... namedtuple_dict_unstructure_factory,
... )
<function namedtuple_dict_unstructure_factory at ...>
Using cattrs.gen
Generators#
The cattrs.gen
module allows for generating and compiling specialized hooks for unstructuring attrs classes, dataclasses and typed dicts.
The default Converter
, upon first encountering one of these types,
will use the generation functions mentioned here to generate specialized hooks for it,
register the hooks and use them.
One reason for generating these hooks in advance is that they can bypass a lot of cattrs machinery and be significantly faster than normal cattrs. The hooks are also good building blocks for more complex customizations.
Another reason is overriding behavior on a per-attribute basis.
Currently, the overrides only support generating dictionary un/structuring hooks (as opposed to tuples), and support omit_if_default
, forbid_extra_keys
, rename
and omit
.
omit_if_default
#
This override can be applied on a per-class or per-attribute basis. The generated unstructuring hook will skip unstructuring values that are equal to their default or factory values.
>>> from cattrs.gen import make_dict_unstructure_fn, override
>>> @define
... class WithDefault:
... a: int
... b: dict = Factory(dict)
>>> c = cattrs.Converter()
>>> c.register_unstructure_hook(WithDefault, make_dict_unstructure_fn(WithDefault, c, b=override(omit_if_default=True)))
>>> c.unstructure(WithDefault(1))
{'a': 1}
Note that the per-attribute value overrides the per-class value.
A side-effect of this is the ability to force the presence of a subset of fields.
For example, consider a class with a dateTime
field and a factory for it: skipping the unstructuring of the dateTime
field would be inconsistent and based on the current time.
So we apply the omit_if_default
rule to the class, but not to the dateTime
field.
Note
The parameter to `make_dict_unstructure_function` is named ``_cattrs_omit_if_default`` instead of just ``omit_if_default`` to avoid potential collisions with an override for a field named ``omit_if_default``.
>>> from datetime import datetime
>>> from cattrs.gen import make_dict_unstructure_fn, override
>>> @define
... class TestClass:
... a: Optional[int] = None
... b: datetime = Factory(datetime.utcnow)
>>> c = cattrs.Converter()
>>> hook = make_dict_unstructure_fn(TestClass, c, _cattrs_omit_if_default=True, b=override(omit_if_default=False))
>>> c.register_unstructure_hook(TestClass, hook)
>>> c.unstructure(TestClass())
{'b': ...}
This override has no effect when generating structuring functions.
forbid_extra_keys
#
By default cattrs is lenient in accepting unstructured input.
If extra keys are present in a dictionary, they will be ignored when generating a structured object.
Sometimes it may be desirable to enforce a stricter contract, and to raise an error when unknown keys are present - in particular when fields have default values this may help with catching typos.
forbid_extra_keys
can also be enabled (or disabled) on a per-class basis when creating structure hooks with make_dict_structure_fn()
.
>>> from cattrs.gen import make_dict_structure_fn
>>>
>>> @define
... class TestClass:
... number: int = 1
>>>
>>> c = cattrs.Converter(forbid_extra_keys=True)
>>> c.structure({"nummber": 2}, TestClass)
Traceback (most recent call last):
...
ForbiddenExtraKeyError: Extra fields in constructor for TestClass: nummber
>>> hook = make_dict_structure_fn(TestClass, c, _cattrs_forbid_extra_keys=False)
>>> c.register_structure_hook(TestClass, hook)
>>> c.structure({"nummber": 2}, TestClass)
TestClass(number=1)
This behavior can only be applied to classes or to the default for the Converter
, and has no effect when generating unstructuring functions.
Changed in version 23.2.0: The value for the make_dict_structure_fn._cattrs_forbid_extra_keys
parameter is now taken from the given converter by default.
rename
#
Using the rename override makes cattrs
use the provided name instead of the real attribute name.
This is useful if an attribute name is a reserved keyword in Python.
>>> from pendulum import DateTime
>>> from cattrs.gen import make_dict_unstructure_fn, make_dict_structure_fn, override
>>> @define
... class ExampleClass:
... klass: Optional[int]
>>> c = cattrs.Converter()
>>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, klass=override(rename="class"))
>>> st_hook = make_dict_structure_fn(ExampleClass, c, klass=override(rename="class"))
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
>>> c.register_structure_hook(ExampleClass, st_hook)
>>> c.unstructure(ExampleClass(1))
{'class': 1}
>>> c.structure({'class': 1}, ExampleClass)
ExampleClass(klass=1)
omit
#
This override can only be applied to individual attributes.
Using the omit
override will simply skip the attribute completely when generating a structuring or unstructuring function.
>>> from cattrs.gen import make_dict_unstructure_fn, override
>>>
>>> @define
... class ExampleClass:
... an_int: int
>>>
>>> c = cattrs.Converter()
>>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, an_int=override(omit=True))
>>> c.register_unstructure_hook(ExampleClass, unst_hook)
>>> c.unstructure(ExampleClass(1))
{}
struct_hook
and unstruct_hook
#
By default, the generators will determine the right un/structure hook for each attribute of a class at time of generation according to the type of each individual attribute.
This process can be overriden by passing in the desired un/structure hook manually.
>>> from cattrs.gen import make_dict_structure_fn, override
>>> @define
... class ExampleClass:
... an_int: int
>>> c = cattrs.Converter()
>>> st_hook = make_dict_structure_fn(
... ExampleClass, c, an_int=override(struct_hook=lambda v, _: v + 1)
... )
>>> c.register_structure_hook(ExampleClass, st_hook)
>>> c.structure({"an_int": 1}, ExampleClass)
ExampleClass(an_int=2)
use_alias
#
By default, fields are un/structured to and from dictionary keys exactly matching the field names.
attrs classes support attrs field aliases, which override the __init__
parameter name for a given field.
By generating your un/structure function with _cattrs_use_alias=True
, cattrs will use the field alias instead of the field name as the un/structured dictionary key.
>>> from cattrs.gen import make_dict_structure_fn
>>>
>>> @define
... class AliasClass:
... number: int = field(default=1, alias="count")
>>>
>>> c = cattrs.Converter()
>>> hook = make_dict_structure_fn(AliasClass, c, _cattrs_use_alias=True)
>>> c.register_structure_hook(AliasClass, hook)
>>> c.structure({"count": 2}, AliasClass)
AliasClass(number=2)
New in version 23.2.0.
include_init_false
#
By default, attrs fields defined as init=False
are skipped when un/structuring.
By generating your un/structure function with _cattrs_include_init_false=True
, all init=False
fields will be included for un/structuring.
>>> from cattrs.gen import make_dict_structure_fn
>>>
>>> @define
... class ClassWithInitFalse:
... number: int = field(default=1, init=False)
>>>
>>> c = cattrs.Converter()
>>> hook = make_dict_structure_fn(ClassWithInitFalse, c, _cattrs_include_init_false=True)
>>> c.register_structure_hook(ClassWithInitFalse, hook)
>>> c.structure({"number": 2}, ClassWithInitFalse)
ClassWithInitFalse(number=2)
A single attribute can be included by overriding it with omit=False
.
>>> c = cattrs.Converter()
>>> hook = make_dict_structure_fn(ClassWithInitFalse, c, number=override(omit=False))
>>> c.register_structure_hook(ClassWithInitFalse, hook)
>>> c.structure({"number": 2}, ClassWithInitFalse)
ClassWithInitFalse(number=2)
New in version 23.2.0.