File: README.md
attrs is the Python package that will bring back the joy of writing classes by relieving you from the drudgery of implementing object protocols (aka dunder methods).
Trusted by NASA for Mars missions since 2020!
Its main goal is to help you to write concise and correct software without slowing down your code.
Sponsors
attrs would not be possible without our amazing sponsors.
Especially those generously supporting us at the The Organization tier and higher:
Example
attrs gives you a class decorator and a way to declaratively define the attributes on that class:
>>> from attrs import asdict, define, make_class, Factory
>>> @define
... class SomeClass:
... a_number: int = 42
... list_of_numbers: list[int] = Factory(list)
...
... def hard_math(self, another_number):
... return self.a_number + sum(self.list_of_numbers) * another_number
>>> sc = SomeClass(1, [1, 2, 3])
>>> sc
SomeClass(a_number=1, list_of_numbers=[1, 2, 3])
>>> sc.hard_math(3)
19
>>> sc == SomeClass(1, [1, 2, 3])
True
>>> sc != SomeClass(2, [3, 2, 1])
True
>>> asdict(sc)
{'a_number': 1, 'list_of_numbers': [1, 2, 3]}
>>> SomeClass()
SomeClass(a_number=42, list_of_numbers=[])
>>> C = make_class("C", ["a", "b"])
>>> C("foo", "bar")
C(a='foo', b='bar')After declaring your attributes, attrs gives you:
- a concise and explicit overview of the class's attributes,
- a nice human-readable
__repr__, - equality-checking methods,
- an initializer,
- and much more,
without writing dull boilerplate code again and again and without runtime performance penalties.
This example uses attrs's modern APIs that have been introduced in version 20.1.0, and the attrs package import name that has been added in version 21.3.0.
The classic APIs (@attr.s, attr.ib, plus their serious-business aliases) and the attr package import name will remain indefinitely.
Check out On The Core API Names for an in-depth explanation!
Hate Type Annotations!?
No problem!
Types are entirely optional with attrs.
Simply assign attrs.field() to the attributes instead of annotating them with types:
from attrs import define, field
@define
class SomeClass:
a_number = field(default=42)
list_of_numbers = field(factory=list)Data Classes
On the tin, attrs might remind you of dataclasses (and indeed, dataclasses are a descendant of attrs).
In practice it does a lot more and is more flexible.
For instance, it allows you to define special handling of NumPy arrays for equality checks, allows more ways to plug into the initialization process, has a replacement for __init_subclass__, and allows for stepping through the generated methods using a debugger.
For more details, please refer to our comparison page, but generally speaking, we are more likely to commit crimes against nature to make things work that one would expect to work, but that are quite complicated in practice.
Project Information
- Changelog
- Documentation
- PyPI
- Source Code
- Contributing
- Third-party Extensions
- Get Help: use the
python-attrstag on Stack Overflow
*attrs* for Enterprise
Available as part of the Tidelift Subscription.
The maintainers of attrs and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications.
Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.
File: docs/api.rst
API Reference
.. module:: attrs
attrs works by decorating a class using attrs.define or attr.s and then defining attributes on the class using attrs.field, attr.ib, or type annotations.
What follows is the dry API explanation for people who understand how attrs works.
If you'd like a hands-on tutorial, have a look at examples.
If you're confused by the many names, please check out names for clarification, but the TL;DR <https://en.wikipedia.org/wiki/TL;DR>_ is that as of version 21.3.0, attrs consists of two top-level package names:
- The classic
attrthat powers the venerableattr.sandattr.ib. - The newer
attrsthat only contains most modern APIs and relies onattrs.defineandattrs.fieldto define your classes.
Additionally, some of the APIs that also exist inattrhave nicer defaults (for example,attrs.asdict).
The attrs namespace is built on top of attr -- which will never go away -- and is just as stable, since it doesn't constitute a rewrite.
To keep repetition low and this document at a reasonable size, the attr namespace is documented on a separate page <api-attr>.
Core
.. autofunction:: attrs.define
.. function:: mutable(same_as_define)
Same as attrs.define.
.. versionadded:: 20.1.0
.. function:: frozen(same_as_define)
Behaves the same as attrs.define but sets frozen=True and on_setattr=None.
.. versionadded:: 20.1.0
.. autofunction:: field
.. autoclass:: Attribute
:members: evolve
For example:
.. doctest::
>>> import attrs
>>> from attrs import define, field
>>> @define
... class C:
... x = field()
>>> attrs.fields(C).x
Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x').. autofunction:: make_class
This is handy if you want to programmatically create classes.
For example:
.. doctest::
>>> C1 = attrs.make_class("C1", ["x", "y"])
>>> C1(1, 2)
C1(x=1, y=2)
>>> C2 = attrs.make_class("C2", {
... "x": field(default=42),
... "y": field(factory=list)
... })
>>> C2()
C2(x=42, y=[]).. autoclass:: Factory
For example:
.. doctest::
>>> @define
... class C:
... x = field(default=attrs.Factory(list))
... y = field(default=attrs.Factory(
... lambda self: set(self.x),
... takes_self=True)
... )
>>> C()
C(x=[], y=set())
>>> C([1, 2, 3])
C(x=[1, 2, 3], y={1, 2, 3}).. autodata:: attrs.NOTHING
:no-value:
Exceptions
.. module:: attrs.exceptions
All exceptions are available from both attr.exceptions and attrs.exceptions and are the same thing.
That means that it doesn't matter from from which namespace they've been raised and/or caught:
.. doctest::
import attrs, attr
try:
... raise attrs.exceptions.FrozenError()
... except attr.exceptions.FrozenError:
... print("this works!")
this works!
.. autoexception:: PythonTooOldError
.. autoexception:: FrozenError
.. autoexception:: FrozenInstanceError
.. autoexception:: FrozenAttributeError
.. autoexception:: AttrsAttributeNotFoundError
.. autoexception:: NotAnAttrsClassError
.. autoexception:: DefaultAlreadySetError
.. autoexception:: NotCallableError
.. autoexception:: UnannotatedAttributeError
For example::
@attr.s(auto_attribs=True)
class C:
x: int
y = attr.ib() # <- ERROR!.. _helpers:
Helpers
attrs comes with a bunch of helper methods that make working with it easier:
.. currentmodule:: attrs
.. autofunction:: attrs.cmp_using
.. autofunction:: attrs.fields
For example:
.. doctest::
>>> @define
... class C:
... x = field()
... y = field()
>>> attrs.fields(C)
(Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y'))
>>> attrs.fields(C(1, 2)) is attrs.fields(C)
True
>>> attrs.fields(C)[1]
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')
>>> attrs.fields(C).y is attrs.fields(C)[1]
True.. autofunction:: attrs.fields_dict
For example:
.. doctest::
>>> @attr.s
... class C:
... x = attr.ib()
... y = attr.ib()
>>> attrs.fields_dict(C)
{'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')}
>>> attr.fields_dict(C)['y']
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')
>>> attrs.fields_dict(C)['y'] is attrs.fields(C).y
True.. autofunction:: attrs.has
For example:
.. doctest::
>>> @define
... class C:
... pass
>>> attrs.has(C)
True
>>> attrs.has(object)
False.. autofunction:: attrs.inspect
For example:
.. doctest::
>>> @define
... class CInspect:
... pass
>>> attrs.inspect(CInspect) # doctest: +ELLIPSIS
ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at ...>, field_transformer=None).. autoclass:: attrs.ClassProps
.. autoclass:: attrs.ClassProps.Hashability
:members: HASHABLE, HASHABLE_CACHED, UNHASHABLE, LEAVE_ALONE
.. autoclass:: attrs.ClassProps.KeywordOnly
:members: NO, YES, FORCE
.. autofunction:: attrs.resolve_types
For example:
.. doctest::
>>> import typing
>>> @define
... class A:
... a: typing.List['A']
... b: 'B'
...
>>> @define
... class B:
... a: A
...
>>> attrs.fields(A).a.type
typing.List[ForwardRef('A')]
>>> attrs.fields(A).b.type
'B'
>>> attrs.resolve_types(A, globals(), locals())
<class 'A'>
>>> attrs.fields(A).a.type
typing.List[A]
>>> attrs.fields(A).b.type
<class 'B'>.. autofunction:: attrs.asdict
For example:
.. doctest::
>>> @define
... class C:
... x: int
... y: int
>>> attrs.asdict(C(1, C(2, 3)))
{'x': 1, 'y': {'x': 2, 'y': 3}}.. autofunction:: attrs.astuple
For example:
.. doctest::
>>> @define
... class C:
... x = field()
... y = field()
>>> attrs.astuple(C(1,2))
(1, 2).. module:: attrs.filters
attrs includes helpers for filtering the attributes in attrs.asdict and attrs.astuple:
.. autofunction:: include
.. autofunction:: exclude
See :func:attrs.asdict for examples.
All objects from attrs.filters are also available from attr.filters (it's the same module in a different namespace).
.. currentmodule:: attrs
.. autofunction:: attrs.evolve
For example:
.. doctest::
>>> @define
... class C:
... x: int
... y: int
>>> i1 = C(1, 2)
>>> i1
C(x=1, y=2)
>>> i2 = attrs.evolve(i1, y=3)
>>> i2
C(x=1, y=3)
>>> i1 == i2
False evolve creates a new instance using __init__.
This fact has several implications:
- private attributes should be specified without the leading underscore, just like in
__init__. - attributes with
init=Falsecan't be set withevolve. - the usual
__init__validators will validate the new values.
.. autofunction:: attrs.validate
For example:
.. doctest::
>>> @define(on_setattr=attrs.setters.NO_OP)
... class C:
... x = field(validator=attrs.validators.instance_of(int))
>>> i = C(1)
>>> i.x = "1"
>>> attrs.validate(i)
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got '1' that is a <class 'str'>).", ...).. _api-validators:
Validators
.. module:: attrs.validators
attrs comes with some common validators in the attrs.validators module.
All objects from attrs.validators are also available from attr.validators (it's the same module in a different namespace).
.. autofunction:: attrs.validators.lt
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.lt(42))
>>> C(41)
C(x=41)
>>> C(42)
Traceback (most recent call last):
...
ValueError: ("'x' must be < 42: 42").. autofunction:: attrs.validators.le
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.le(42))
>>> C(42)
C(x=42)
>>> C(43)
Traceback (most recent call last):
...
ValueError: ("'x' must be <= 42: 43").. autofunction:: attrs.validators.ge
For example:
.. doctest::
>>> @define
... class C:
... x = attrs.field(validator=attrs.validators.ge(42))
>>> C(42)
C(x=42)
>>> C(41)
Traceback (most recent call last):
...
ValueError: ("'x' must be => 42: 41").. autofunction:: attrs.validators.gt
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.gt(42))
>>> C(43)
C(x=43)
>>> C(42)
Traceback (most recent call last):
...
ValueError: ("'x' must be > 42: 42").. autofunction:: attrs.validators.ne
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.ne(42))
>>> C(43)
C(x=43)
>>> C(42)
Traceback (most recent call last):
...
ValueError: ("'x' must be != 42: 42").. autofunction:: attrs.validators.max_len
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.max_len(4))
>>> C("spam")
C(x='spam')
>>> C("bacon")
Traceback (most recent call last):
...
ValueError: ("Length of 'x' must be <= 4: 5").. autofunction:: attrs.validators.min_len
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.min_len(1))
>>> C("bacon")
C(x='bacon')
>>> C("")
Traceback (most recent call last):
...
ValueError: ("Length of 'x' must be => 1: 0").. autofunction:: attrs.validators.instance_of
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=>, type=None, kw_only=False), <type 'int'>, '42')
>>> C(None)
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got None that is a <type 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=>, repr=True, cmp=True, hash=None, init=True, type=None, kw_only=False), <type 'int'>, None).. autofunction:: attrs.validators.in_
For example:
.. doctest::
>>> import enum
>>> class State(enum.Enum):
... ON = "on"
... OFF = "off"
>>> @define
... class C:
... state = field(validator=attrs.validators.in_(State))
... val = field(validator=attrs.validators.in_([1, 2, 3]))
>>> C(State.ON, 1)
C(state=<State.ON: 'on'>, val=1)
>>> C("On", 1)
Traceback (most recent call last):
...
ValueError: 'state' must be in <enum 'State'> (got 'On'), Attribute(name='state', default=NOTHING, validator=>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), <enum 'State'>, 'on')
>>> C(State.ON, 4)
Traceback (most recent call last):
...
ValueError: 'val' must be in [1, 2, 3] (got 4), Attribute(name='val', default=NOTHING, validator=, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), [1, 2, 3], 4).. autofunction:: attrs.validators.and_
For convenience, it's also possible to pass a list to attrs.field's validator argument.
Thus the following two statements are equivalent::
x = field(validator=attrs.validators.and_(v1, v2, v3))
x = field(validator=[v1, v2, v3]).. autofunction:: attrs.validators.or_
For example:
.. doctest::
>>> @define
... class C:
... val: int | list[int] = field(
... validator=attrs.validators.or_(
... attrs.validators.instance_of(int),
... attrs.validators.deep_iterable(attrs.validators.instance_of(int)),
... )
... )
>>> C(42)
C(val=42)
>>> C([1, 2, 3])
C(val=[1, 2, 3])
>>> C(val='42')
Traceback (most recent call last):
...
ValueError: None of (>, <deep_iterable validator for iterables of >>) satisfied for value '42'.. autofunction:: attrs.validators.not_
For example:
.. doctest::
>>> reserved_names = {"id", "time", "source"}
>>> @define
... class Measurement:
... tags = field(
... validator=attrs.validators.deep_mapping(
... key_validator=attrs.validators.not_(
... attrs.validators.in_(reserved_names),
... msg="reserved tag key",
... ),
... value_validator=attrs.validators.instance_of((str, int)),
... )
... )
>>> Measurement(tags={"source": "universe"})
Traceback (most recent call last):
...
ValueError: ("reserved tag key", Attribute(name='tags', default=NOTHING, validator=<not_ validator wrapping , capturing (<class 'ValueError'>, <class 'TypeError'>)>, type=None, kw_only=False), , {'source_': 'universe'}, (<class 'ValueError'>, <class 'TypeError'>))
>>> Measurement(tags={"source_": "universe"})
Measurement(tags={'source_': 'universe'}).. autofunction:: attrs.validators.optional
For example:
.. doctest::
>>> @define
... class C:
... x = field(
... validator=attrs.validators.optional(
... attrs.validators.instance_of(int)
... ))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=>, type=None, kw_only=False), <type 'int'>, '42')
>>> C(None)
C(x=None).. autofunction:: attrs.validators.is_callable
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.is_callable())
>>> C(isinstance)
C(x=)
>>> C("not a callable")
Traceback (most recent call last):
...
attr.exceptions.NotCallableError: 'x' must be callable (got 'not a callable' that is a <class 'str'>)... autofunction:: attrs.validators.matches_re
For example:
.. doctest::
>>> @define
... class User:
... email = field(validator=attrs.validators.matches_re(
... r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"))
>>> User(email="[email protected]")
User(email='[email protected]')
>>> User(email="[email protected]@test.com")
Traceback (most recent call last):
...
ValueError: ("'email' must match regex '(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\\\.[a-zA-Z0-9-.]+$)' ('[email protected]@test.com' doesn't)", Attribute(name='email', default=NOTHING, validator=<matches_re validator for pattern re.compile('(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)')>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), re.compile('(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)'), '[email protected]@test.com').. autofunction:: attrs.validators.deep_iterable
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.deep_iterable(
... member_validator=attrs.validators.instance_of(int),
... iterable_validator=attrs.validators.instance_of(list)
... ))
>>> C(x=[1, 2, 3])
C(x=[1, 2, 3])
>>> C(x=set([1, 2, 3]))
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'list'> (got {1, 2, 3} that is a <class 'set'>).", Attribute(name='x', default=NOTHING, validator=<deep_iterable validator for > iterables of >>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'list'>, {1, 2, 3})
>>> C(x=[1, 2, "3"])
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got '3' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=<deep_iterable validator for > iterables of >>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'int'>, '3').. autofunction:: attrs.validators.deep_mapping
For example:
.. doctest::
>>> @define
... class C:
... x = field(validator=attrs.validators.deep_mapping(
... key_validator=attrs.validators.instance_of(str),
... value_validator=attrs.validators.instance_of(int),
... mapping_validator=attrs.validators.instance_of(dict)
... ))
>>> C(x={"a": 1, "b": 2})
C(x={'a': 1, 'b': 2})
>>> C(x=None)
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'dict'> (got None that is a <class 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=<deep_mapping validator for objects mapping > to >>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'dict'>, None)
>>> C(x={"a": 1.0, "b": 2})
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got 1.0 that is a <class 'float'>).", Attribute(name='x', default=NOTHING, validator=<deep_mapping validator for objects mapping > to >>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'int'>, 1.0)
>>> C(x={"a": 1, 7: 2})
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'str'> (got 7 that is a <class 'int'>).", Attribute(name='x', default=NOTHING, validator=<deep_mapping validator for objects mapping > to >>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), <class 'str'>, 7)Validators can be both globally and locally disabled:
.. autofunction:: attrs.validators.set_disabled
.. autofunction:: attrs.validators.get_disabled
.. autofunction:: attrs.validators.disabled
Converters
.. autoclass:: attrs.Converter
For example:
.. doctest::
>>> def complicated(value, self_, field):
... return int(value) * self_.factor + field.metadata["offset"]
>>> @define
... class C:
... factor = 5 # not an *attrs* field
... x = field(
... metadata={"offset": 200},
... converter=attrs.Converter(
... complicated,
... takes_self=True, takes_field=True
... ))
>>> C("42")
C(x=410).. module:: attrs.converters
All objects from attrs.converters are also available from attr.converters (it's the same module in a different namespace).
.. autofunction:: attrs.converters.pipe
For convenience, it's also possible to pass a list to attrs.field / attr.ib's converter arguments.
Thus the following two statements are equivalent::
x = attrs.field(converter=attrs.converter.pipe(c1, c2, c3))
x = attrs.field(converter=[c1, c2, c3]).. autofunction:: attrs.converters.optional
For example:
.. doctest::
>>> @define
... class C:
... x = field(converter=attrs.converters.optional(int))
>>> C(None)
C(x=None)
>>> C(42)
C(x=42).. autofunction:: attrs.converters.default_if_none
For example:
.. doctest::
>>> @define
... class C:
... x = field(
... converter=attrs.converters.default_if_none("")
... )
>>> C(None)
C(x='').. autofunction:: attrs.converters.to_bool(val)
For example:
.. doctest::
>>> @define
... class C:
... x = field(
... converter=attrs.converters.to_bool
... )
>>> C("yes")
C(x=True)
>>> C(0)
C(x=False)
>>> C("norway")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Cannot convert value to bool: norway.. _api_setters:
Setters
.. module:: attrs.setters
These are helpers that you can use together with attrs.define's and attrs.field's on_setattr arguments.
All setters in attrs.setters are also available from attr.setters (it's the same module in a different namespace).
.. autofunction:: frozen
.. autofunction:: validate
.. autofunction:: convert
.. autofunction:: pipe
.. data:: NO_OP
Sentinel for disabling class-wide on_setattr hooks for certain attributes.
Does not work in attrs.setters.pipe or within lists.
.. versionadded:: 20.1.0
For example, only x is frozen here:
.. doctest::
>>> @define(on_setattr=attr.setters.frozen)
... class C:
... x = field()
... y = field(on_setattr=attr.setters.NO_OP)
>>> c = C(1, 2)
>>> c.y = 3
>>> c.y
3
>>> c.x = 4
Traceback (most recent call last):
...
attrs.exceptions.FrozenAttributeError: () .. tip::
Use attrs.define's frozen argument (or attrs.frozen) to freeze whole classes; it is more efficient.
File: docs/api-attr.rst
API Reference for the ``attr`` Namespace
.. note::
These are the traditional APIs whose creation predates type annotations.
They are not deprecated, but we suggest using the :mod:attrs namespace for new code, because they look nicer and have better defaults.
See also :doc:names.
.. module:: attr
Core
.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True, unsafe_hash=None)
For example:
.. doctest::
>>> import attr
>>> @attr.s
... class C:
... _private = attr.ib()
>>> C(private=42)
C(_private=42)
>>> class D:
... def __init__(self, x):
... self.x = x
>>> D(1)
<D object at ...>
>>> D = attr.s(these={"x": attr.ib()}, init=False)(D)
>>> D(1)
D(x=1)
>>> @attr.s(auto_exc=True)
... class Error(Exception):
... x = attr.ib()
... y = attr.ib(default=42, init=False)
>>> Error("foo")
Error(x='foo', y=42)
>>> raise Error("foo")
Traceback (most recent call last):
...
Error: ('foo', 42)
>>> raise ValueError("foo", 42) # for comparison
Traceback (most recent call last):
...
ValueError: ('foo', 42).. autofunction:: attr.ib
.. note::
*attrs* also comes with a serious-business alias ``attr.attrib``. The object returned by attr.ib also allows for setting the default and the validator using decorators:
.. doctest::
>>> @attr.s
... class C:
... x = attr.ib()
... y = attr.ib()
... @x.validator
... def _any_name_except_a_name_of_an_attribute(self, attribute, value):
... if value < 0:
... raise ValueError("x must be positive")
... @y.default
... def _any_name_except_a_name_of_an_attribute(self):
... return self.x + 1
>>> C(1)
C(x=1, y=2)
>>> C(-1)
Traceback (most recent call last):
...
ValueError: x must be positive.. function:: attrs
Serious business alias for attr.s.
.. function:: define
Same as attrs.define.
.. function:: mutable
Same as attrs.mutable.
.. function:: frozen
Same as attrs.frozen.
.. function:: field
Same as attrs.field.
.. class:: Attribute
Same as attrs.Attribute.
.. function:: make_class
Same as attrs.make_class.
.. autoclass:: Factory
:noindex:
Same as attrs.Factory.
.. data:: NOTHING
Same as attrs.NOTHING.
Exceptions
.. module:: attr.exceptions
All exceptions are available from both attr.exceptions and attrs.exceptions (it's the same module in a different namespace).
Please refer to attrs.exceptions for details.
Helpers
.. currentmodule:: attr
.. function:: cmp_using
Same as attrs.cmp_using.
.. function:: fields
Same as attrs.fields.
.. function:: fields_dict
Same as attrs.fields_dict.
.. function:: has
Same as attrs.has.
.. function:: resolve_types
Same as attrs.resolve_types.
.. autofunction:: asdict
.. autofunction:: astuple
.. module:: attr.filters
.. function:: include
Same as attrs.filters.include.
.. function:: exclude
Same as attrs.filters.exclude.
See :func:attrs.asdict for examples.
All objects from attrs.filters are also available in attr.filters.
.. currentmodule:: attr
.. function:: evolve
Same as attrs.evolve.
.. function:: validate
Same as attrs.validate.
Validators
.. module:: attr.validators
All objects from attrs.validators are also available in attr.validators.
Please refer to the former for details.
Converters
.. module:: attr.converters
All objects from attrs.converters are also available from attr.converters.
Please refer to the former for details.
Setters
.. module:: attr.setters
All objects from attrs.setters are also available in attr.setters.
Please refer to the former for details.
Deprecated APIs
.. currentmodule:: attr
To help you write backward compatible code that doesn't throw warnings on modern releases, the attr module has an __version_info__ attribute as of version 19.2.0.
It behaves similarly to sys.version_info and is an instance of attr.VersionInfo:
.. autoclass:: VersionInfo
With its help you can write code like this:
if getattr(attr, "version_info", (0,)) >= (19, 2):
... cmp_off = {"eq": False}
... else:
... cmp_off = {"cmp": False}
cmp_off == {"eq": False}
True
@attr.s(**cmp_off)
... class C:
... pass
.. autofunction:: assoc
Before attrs got attrs.validators.set_disabled and attrs.validators.set_disabled, it had the following APIs to globally enable and disable validators.
They won't be removed, but are discouraged to use:
.. autofunction:: set_run_validators
.. autofunction:: get_run_validators
The serious-business aliases used to be called attr.attributes and attr.attr.
There are no plans to remove them but they shouldn't be used in new code.
File: docs/comparison.md
Comparison
By default, two instances of attrs classes are equal if they have the same type and all their fields are equal.
For that, attrs writes __eq__ and __ne__ methods for you.
Additionally, if you pass order=True, attrs will also create a complete set of ordering methods: __le__, __lt__, __ge__, and __gt__.
For equality, attrs will generate a statement comparing the types of both instances,
and then comparing each attribute in turn using ==.
For order, attrs will:
- Check if the types of the instances you're comparing are equal,
- if so, create a tuple of all field values for each instance,
- and finally perform the desired comparison operation on those tuples.
(custom-comparison)=
Customization
As with other features, you can exclude fields from being involved in comparison operations:
>>> from attrs import define, field
>>> @define
... class C:
... x: int
... y: int = field(eq=False)
>>> C(1, 2) == C(1, 3)
TrueAdditionally you can also pass a callable instead of a bool to both eq and order.
It is then used as a key function like you may know from {func}sorted:
>>> @define
... class S:
... x: str = field(eq=str.lower)
>>> S("foo") == S("FOO")
True
>>> @define(order=True)
... class C:
... x: str = field(order=int)
>>> C("10") > C("2")
TrueThis is especially useful when you have fields with objects that have atypical comparison properties.
Common examples of such objects are NumPy arrays.
To save you unnecessary boilerplate, attrs comes with the {func}attrs.cmp_using helper to create such functions.
For NumPy arrays it would look like this:
import numpy
@define
class C:
an_array = field(eq=attrs.cmp_using(eq=numpy.array_equal)):::{warning}
Please note that eq and order are set independently, because order is False by default in {func}~attrs.define (but not in {func}attr.s).
You can set both at once by using the cmp argument that we've undeprecated just for this use-case.
:::
File: docs/examples.md
*attrs* by Example
Basics
The simplest possible usage is:
>>> from attrs import define, field
>>> @define
... class Empty:
... pass
>>> Empty()
Empty()
>>> Empty() == Empty()
True
>>> Empty() is Empty()
FalseSo in other words: attrs is useful even without actual {term}fields <field>!
But you'll usually want some data on your classes, so let's add some:
>>> @define
... class Coordinates:
... x: int
... y: intBy default, all features are added, so you immediately have a fully functional data class with a nice repr string and comparison methods.
>>> c1 = Coordinates(1, 2)
>>> c1
Coordinates(x=1, y=2)
>>> c2 = Coordinates(x=2, y=1)
>>> c2
Coordinates(x=2, y=1)
>>> c1 == c2
FalseAs shown, the generated __init__ method allows for both positional and keyword arguments.
Unlike Data Classes, attrs doesn't force you to use type annotations.
So, the previous example could also have been written as:
>>> @define
... class Coordinates:
... x = field()
... y = field()
>>> Coordinates(1, 2)
Coordinates(x=1, y=2):::{caution}
If a class body contains a field that is defined using {func}attrs.field (or {func}attr.ib), but lacks a type annotation, attrs switches to a no-typing mode and ignores fields that have type annotations but are not defined using {func}attrs.field (or {func}attr.ib).
:::
For private attributes, attrs will strip the leading underscores for keyword arguments:
>>> @define
... class C:
... _x: int
>>> C(x=1)
C(_x=1)If you want to initialize your private attributes yourself, you can do that too:
>>> @define
... class C:
... _x: int = field(init=False, default=42)
>>> C()
C(_x=42)
>>> C(23)
Traceback (most recent call last):
...
TypeError: __init__() takes exactly 1 argument (2 given)If you prefer to expose your privates, you can use keyword argument aliases:
>>> @define
... class C:
... _x: int = field(alias="_x")
>>> C(_x=1)
C(_x=1)An additional way of defining attributes is supported too.
This is useful in times when you want to enhance classes that are not yours (nice __repr__ for Django models anyone?):
>>> class SomethingFromSomeoneElse:
... def __init__(self, x):
... self.x = x
>>> SomethingFromSomeoneElse = define(
... these={
... "x": field()
... }, init=False)(SomethingFromSomeoneElse)
>>> SomethingFromSomeoneElse(1)
SomethingFromSomeoneElse(x=1)Subclassing is bad for you (except when doing strict specialization), but attrs will still do what you'd hope for:
>>> @define(slots=False)
... class A:
... a: int
... def get_a(self):
... return self.a
>>> @define(slots=False)
... class B:
... b: int
>>> @define(slots=False)
... class C(B, A):
... c: int
>>> i = C(1, 2, 3)
>>> i
C(a=1, b=2, c=3)
>>> i == C(1, 2, 3)
True
>>> i.get_a()
1{term}Slotted classes <slotted classes>, which are the default for the new APIs, don't play well with multiple inheritance so we don't use them in the example.
The order of the attributes is defined by the MRO.
Keyword-only Attributes
You can also add keyword-only attributes:
>>> @define
... class A:
... a: int = field(kw_only=True)
>>> A()
Traceback (most recent call last):
...
TypeError: A() missing 1 required keyword-only argument: 'a'
>>> A(a=1)
A(a=1)kw_only may also be specified at decorator level, and will apply to all attributes:
>>> @define(kw_only=True)
... class A:
... a: int
... b: int
>>> A(1, 2)
Traceback (most recent call last):
...
TypeError: __init__() takes 1 positional argument but 3 were given
>>> A(a=1, b=2)
A(a=1, b=2)If you create an attribute with init=False, the kw_only argument is ignored.
Keyword-only attributes allow subclasses to add attributes without default values, even if the base class defines attributes with default values:
>>> @define
... class A:
... a: int = 0
>>> @define
... class B(A):
... b: int = field(kw_only=True)
>>> B(b=1)
B(a=0, b=1)
>>> B()
Traceback (most recent call last):
...
TypeError: B() missing 1 required keyword-only argument: 'b'If you don't set kw_only=True, then there is no valid attribute ordering, and you'll get an error:
>>> @define
... class A:
... a: int = 0
>>> @define
... class B(A):
... b: int
Traceback (most recent call last):
...
ValueError: No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: Attribute(name='b', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, converter=None, metadata=mappingproxy({}), type=int, kw_only=False)(asdict)=
Converting to Collections Types
When you have a class with data, it often is very convenient to transform that class into a {class}dict (for example if you want to serialize it to JSON):
>>> from attrs import asdict
>>> asdict(Coordinates(x=1, y=2))
{'x': 1, 'y': 2}Some fields cannot or should not be transformed.
For that, {func}attrs.asdict offers a callback that decides whether an attribute should be included:
>>> @define
... class User:
... email: str
... password: str
>>> @define
... class UserList:
... users: list[User]
>>> asdict(UserList([User("[email protected]", "s33kred"),
... User("[email protected]", "p4ssw0rd")]),
... filter=lambda attr, value: attr.name != "password")
{'users': [{'email': '[email protected]'}, {'email': '[email protected]'}]}For the common case where you want to include or exclude certain types, string name or attributes, attrs ships with a few helpers:
>>> from attrs import asdict, filters, fields
>>> @define
... class User:
... login: str
... password: str
... email: str
... id: int
>>> asdict(
... User("jane", "s33kred", "[email protected]", 42),
... filter=filters.exclude(fields(User).password, "email", int))
{'login': 'jane'}
>>> @define
... class C:
... x: str
... y: str
... z: int
>>> asdict(C("foo", "2", 3),
... filter=filters.include(int, fields(C).x))
{'x': 'foo', 'z': 3}
>>> asdict(C("foo", "2", 3),
... filter=filters.include(fields(C).x, "z"))
{'x': 'foo', 'z': 3}:::{note}
Though using string names directly is convenient, mistyping attribute names will silently do the wrong thing and neither Python nor your type checker can help you.
{func}attrs.fields() will raise an AttributeError when the field doesn't exist while literal string names won't.
Using {func}attrs.fields() to get attributes is worth being recommended in most cases.
>>> asdict(
... User("jane", "s33kred", "[email protected]", 42),
... filter=filters.exclude("passwd")
... )
{'login': 'jane', 'password': 's33kred', 'email': '[email protected]', 'id': 42}
>>> asdict(
... User("jane", "s33kred", "[email protected]", 42),
... filter=fields(User).passwd
... )
Traceback (most recent call last):
...
AttributeError: 'UserAttributes' object has no attribute 'passwd'. Did you mean: 'password'?:::
Other times, all you want is a tuple and attrs won't let you down:
>>> import sqlite3
>>> from attrs import astuple
>>> @define
... class Foo:
... a: int
... b: int
>>> foo = Foo(2, 3)
>>> with sqlite3.connect(":memory:") as conn:
... c = conn.cursor()
... c.execute("CREATE TABLE foo (x INTEGER PRIMARY KEY ASC, y)") #doctest: +ELLIPSIS
... c.execute("INSERT INTO foo VALUES (?, ?)", astuple(foo)) #doctest: +ELLIPSIS
... foo2 = Foo(*c.execute("SELECT x, y FROM foo").fetchone())
<sqlite3.Cursor object at ...>
<sqlite3.Cursor object at ...>
>>> foo == foo2
TrueFor more advanced transformations and conversions, we recommend you look at a companion library (such as cattrs).
Defaults
Sometimes you want to have default values for your initializer.
And sometimes you even want mutable objects as default values (ever accidentally used def f(arg=[])?).
attrs has you covered in both cases:
>>> import collections
>>> @define
... class Connection:
... socket: int
... @classmethod
... def connect(cls, db_string):
... # ... connect somehow to db_string ...
... return cls(socket=42)
>>> @define
... class ConnectionPool:
... db_string: str
... pool: collections.deque = Factory(collections.deque)
... debug: bool = False
... def get_connection(self):
... try:
... return self.pool.pop()
... except IndexError:
... if self.debug:
... print("New connection!")
... return Connection.connect(self.db_string)
... def free_connection(self, conn):
... if self.debug:
... print("Connection returned!")
... self.pool.appendleft(conn)
...
>>> cp = ConnectionPool("postgres://localhost")
>>> cp
ConnectionPool(db_string='postgres://localhost', pool=deque([]), debug=False)
>>> conn = cp.get_connection()
>>> conn
Connection(socket=42)
>>> cp.free_connection(conn)
>>> cp
ConnectionPool(db_string='postgres://localhost', pool=deque([Connection(socket=42)]), debug=False)More information on why class methods for constructing objects are awesome can be found in this insightful blog post.
Default factories can also be set using the factory argument to {func}~attrs.field, and using a decorator.
The method receives the partially initialized instance which enables you to base a default value on other attributes:
>>> @define
... class C:
... x: int = 1
... y: int = field()
... @y.default
... def _any_name_except_a_name_of_an_attribute(self):
... return self.x + 1
... z: list = field(factory=list)
>>> C()
C(x=1, y=2, z=[])Please keep in mind that the decorator approach only works if the attribute in question has a {func}~attrs.field assigned to it.
As a result, annotating an attribute with a type is not enough if you use @default.
(examples-validators)=
Validators
Although your initializers should do as little as possible (ideally: just initialize your instance according to the arguments!), it can come in handy to do some kind of validation on the arguments.
attrs offers two ways to define validators for each attribute and it's up to you to choose which one suits your style and project better.
You can use a decorator:
>>> @define
... class C:
... x: int = field()
... @x.validator
... def check(self, attribute, value):
... if value > 42:
... raise ValueError("x must be smaller or equal to 42")
>>> C(42)
C(x=42)
>>> C(43)
Traceback (most recent call last):
...
ValueError: x must be smaller or equal to 42...or a callable...
>>> from attrs import validators
>>> def x_smaller_than_y(instance, attribute, value):
... if value >= instance.y:
... raise ValueError("'x' has to be smaller than 'y'!")
>>> @define
... class C:
... x: int = field(validator=[validators.instance_of(int),
... x_smaller_than_y])
... y: int
>>> C(x=3, y=4)
C(x=3, y=4)
>>> C(x=4, y=3)
Traceback (most recent call last):
...
ValueError: 'x' has to be smaller than 'y'!...or both at once:
>>> @define
... class C:
... x: int = field(validator=validators.instance_of(int))
... @x.validator
... def fits_byte(self, attribute, value):
... if not 0 <= value < 256:
... raise ValueError("value out of bounds")
>>> C(128)
C(x=128)
>>> C("128")
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=int, converter=None, kw_only=False), <class 'int'>, '128')
>>> C(256)
Traceback (most recent call last):
...
ValueError: value out of boundsPlease note that the decorator approach only works if -- and only if! -- the attribute in question has a {func}~attrs.field assigned.
Therefore if you use @validator, it is not enough to annotate said attribute with a type.
attrs ships with a bunch of validators, make sure to check them out before writing your own:
>>> @define
... class C:
... x: int = field(validator=validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=>, type=None, kw_only=False), <type 'int'>, '42')If using the old-school {func}attr.s decorator, validators only run on initialization by default.
If using the newer {func}attrs.define and friends, validators run on initialization and on attribute setting.
This behavior can be changed using the on_setattr argument.
Check out {ref}validators for more details.
Conversion
Attributes can have a converter function specified, which will be called with the attribute's passed-in value to get a new value to use.
This can be useful for doing type-conversions on values that you don't want to force your callers to do.
>>> @define
... class C:
... x: int = field(converter=int)
>>> o = C("1")
>>> o.x
1
>>> o.x = "2"
>>> o.x
2If using the old-school {func}attr.s decorator, converters only run on initialization by default.
If using the newer {func}attrs.define and friends, converters run on initialization and on attribute setting.
This behavior can be changed using the on_setattr argument.
Check out {ref}converters for more details.
Running Custom Code Before/After Setting Attributes
If your __setattr__ hook needs to run custom code before and after setting attributes, you can pass a generator to the on_setattr argument:
>>> from typing import Generator
>>> def gen_hook(instance: Any, attribute: attr.Attribute[int], value: Any) -> Generator[int]:
... print(f"value coming in: {value!r}")
... new_value = int(value) * 2 + 286
... yield new_value
... print(f"set {attribute.name} to {new_value!r}")
>>> @define
... class C:
... x: int = field(on_setattr=gen_hook)
>>> c = C(42) # doesn't run on __init__
>>> c.x = "67"
value coming in: '67'
set x to 420
>>> c.x
420(metadata)=
Metadata
All attrs attributes may include arbitrary metadata in the form of a read-only dictionary.
>>> from attrs import fields
>>> @define
... class C:
... x = field(metadata={'my_metadata': 1})
>>> fields(C).x.metadata
mappingproxy({'my_metadata': 1})
>>> fields(C).x.metadata['my_metadata']
1Metadata is not used by attrs, and is meant to enable rich functionality in third-party libraries.
The metadata dictionary follows the normal dictionary rules:
Keys need to be hashable, and both keys and values are recommended to be immutable.
If you're the author of a third-party library with attrs integration, please see Extending Metadata.
Types
attrs also allows you to associate a type with an attribute using either the type argument to using {pep}526-annotations or {func}attrs.field/{func}attr.ib:
>>> @define
... class C:
... x: int
>>> fields(C).x.type
<class 'int'>
>>> import attr
>>> @attr.s
... class C:
... x = attr.ib(type=int)
>>> fields(C).x.type
<class 'int'>If you don't mind annotating all attributes, you can even drop the attrs.field and assign default values instead:
>>> import typing
>>> @define
... class AutoC:
... cls_var: typing.ClassVar[int] = 5 # this one is ignored
... l: list[int] = Factory(list)
... x: int = 1
... foo: str = "every attrib needs a type if auto_attribs=True"
... bar: typing.Any = None
>>> fields(AutoC).l.type
list[int]
>>> fields(AutoC).x.type
<class 'int'>
>>> fields(AutoC).foo.type
<class 'str'>
>>> fields(AutoC).bar.type
typing.Any
>>> AutoC()
AutoC(l=[], x=1, foo='every attrib needs a type if auto_attribs=True', bar=None)
>>> AutoC.cls_var
5The generated __init__ method will have an attribute called __annotations__ that contains this type information.
If your annotations contain strings (for example, forward references),
you can resolve these after all references have been defined by using {func}attrs.resolve_types.
This will replace the type attribute in the respective fields.
>>> from attrs import resolve_types
>>> @define
... class A:
... a: 'list[A]'
... b: 'B'
...
>>> @define
... class B:
... a: A
...
>>> fields(A).a.type
'list[A]'
>>> fields(A).b.type
'B'
>>> resolve_types(A, globals(), locals())
<class 'A'>
>>> fields(A).a.type
list[A]
>>> fields(A).b.type
<class 'B'>:::{note}
If you find yourself using string type annotations to handle forward references, wrap the entire type annotation in quotes instead of only the type you need a forward reference to (so 'list[A]' instead of list['A']).
This is a limitation of the Python typing system.
:::
:::{warning}
attrs itself doesn't have any features that work on top of type metadata.
However it's useful for writing your own validators or serialization frameworks.
:::
Slots
{term}Slotted classes <slotted classes> have several advantages on CPython.
Defining __slots__ by hand is tedious, in attrs it's just a matter of using {func}attrs.define or passing slots=True to {func}attr.s:
>>> @define
... class Coordinates:
... x: int
... y: int
>>> import attr
>>> @attr.s(slots=True)
... class Coordinates:
... x: int
... y: int{func}~attrs.define sets slots=True by default.
Immutability
Sometimes you have instances that shouldn't be changed after instantiation.
Immutability is especially popular in functional programming and is generally a very good thing.
If you'd like to enforce it, attrs will try to help:
>>> from attrs import frozen
>>> @frozen
... class C:
... x: int
>>> i = C(1)
>>> i.x = 2
Traceback (most recent call last):
...
attrs.exceptions.FrozenInstanceError: can't set attribute
>>> i.x
1Please note that true immutability is impossible in Python but it will get you 99% there.
By themselves, immutable classes are useful for long-lived objects that should never change; like configurations for example.
In order to use them in regular program flow, you'll need a way to easily create new instances with changed attributes.
In Clojure that function is called assoc and attrs shamelessly imitates it: {func}attrs.evolve:
>>> from attrs import evolve, frozen
>>> @frozen
... class C:
... x: int
... y: int
>>> i1 = C(1, 2)
>>> i1
C(x=1, y=2)
>>> i2 = evolve(i1, y=3)
>>> i2
C(x=1, y=3)
>>> i1 == i2
FalseOn Python 3.13 and later, you can also use {func}copy.replace from the standard library:
>>> import copy
>>> @frozen
... class C:
... x: int
... y: int
>>> i = C(1, 2)
>>> copy.replace(i, y=3)
C(x=1, y=3)Other Goodies
When building systems that have something resembling a plugin interface, you may want to have a registry of all classes that implement a certain interface:
>>> REGISTRY = []
>>> class Base: # does NOT have to be an attrs class!
... @classmethod
... def __attrs_init_subclass__(cls):
... REGISTRY.append(cls)
>>> @define
... class Impl(Base):
... pass
>>> REGISTRY
[<class 'Impl'>]Sometimes you may want to create a class programmatically.
attrs gives you {func}attrs.make_class for that:
>>> from attrs import make_class
>>> @define
... class C1:
... x = field(type=int)
... y = field()
>>> C2 = make_class("C2", {"x": field(type=int), "y": field()})
>>> fields(C1) == fields(C2)
True
>>> fields(C2).x.type
<class 'int'>You can still have power over the attributes if you pass a dictionary of name: {func}~attrs.field mappings and can pass the same arguments as you can to @attrs.define:
>>> C = make_class("C", {"x": field(default=42),
... "y": field(default=Factory(list))},
... repr=False)
>>> i = C()
>>> i # no repr added!
<__main__.C object at ...>
>>> i.x
42
>>> i.y
[]If you need to dynamically make a class with {func}~attrs.make_class and it needs to be a subclass of something else than {class}object, use the bases argument:
>>> class D:
... def __eq__(self, other):
... return True # arbitrary example
>>> C = make_class("C", {}, bases=(D,), cmp=False)
>>> isinstance(C(), D)
TrueSometimes, you want to have your class's __init__ method do more than just
the initialization, validation, etc. that gets done for you automatically when
using @define.
To do this, just define a __attrs_post_init__ method in your class.
It will get called at the end of the generated __init__ method.
>>> @define
... class C:
... x: int
... y: int
... z: int = field(init=False)
...
... def __attrs_post_init__(self):
... self.z = self.x + self.y
>>> obj = C(x=1, y=2)
>>> obj
C(x=1, y=2, z=3)You can exclude single attributes from certain methods:
>>> @define
... class C:
... user: str
... password: str = field(repr=False)
>>> C("me", "s3kr3t")
C(user='me')Alternatively, to influence how the generated __repr__() method formats a specific attribute, specify a custom callable to be used instead of the repr() built-in function:
>>> @define
... class C:
... user: str
... password: str = field(repr=lambda value: '***')
>>> C("me", "s3kr3t")
C(user='me', password=***)File: docs/extending.md
Extending
Each attrs-decorated class has a __attrs_attrs__ class attribute.
It's a tuple of {class}attrs.Attribute carrying metadata about each attribute.
So it is fairly simple to build your own decorators on top of attrs:
>>> from attrs import define
>>> def print_attrs(cls):
... print(cls.__attrs_attrs__)
... return cls
>>> @print_attrs
... @define
... class C:
... a: int
(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=<class 'int'>, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='a'),):::{warning}
The {func}attrs.define / {func}attr.s decorator must be applied first because it puts __attrs_attrs__ in place!
That means that is has to come after your decorator because:
@a
@b
def f():
passis just syntactic sugar for:
def original_f():
pass
f = a(b(original_f)):::
Wrapping the Decorator
A more elegant way can be to wrap attrs altogether and build a class DSL on top of it.
An example for that is the package environ-config that uses attrs under the hood to define environment-based configurations declaratively without exposing attrs APIs at all.
Another common use case is to overwrite attrs's defaults.
Mypy
Unfortunately, decorator wrapping currently confuses Mypy's attrs plugin.
At the moment, the best workaround is to hold your nose, write a fake Mypy plugin, and mutate a bunch of global variables:
from mypy.plugin import Plugin
from mypy.plugins.attrs import (
attr_attrib_makers,
attr_class_makers,
attr_dataclass_makers,
)
# These work just like `attr.dataclass`.
attr_dataclass_makers.add("my_module.method_looks_like_attr_dataclass")
# This works just like `attr.s`.
attr_class_makers.add("my_module.method_looks_like_attr_s")
# These are our `attr.ib` makers.
attr_attrib_makers.add("my_module.method_looks_like_attrib")
class MyPlugin(Plugin):
# Our plugin does nothing but it has to exist so this file gets loaded.
pass
def plugin(version):
return MyPluginThen tell Mypy about your plugin using your project's mypy.ini:
[mypy]
plugins=:::{warning}
Please note that it is currently impossible to let Mypy know that you've changed defaults like eq or order.
You can only use this trick to tell Mypy that a class is actually an attrs class.
:::
Pyright
Generic decorator wrapping is supported in Pyright via typing.dataclass_transform / {pep}681.
For a custom wrapping of the form:
@typing.dataclass_transform(field_specifiers=(attr.attrib, attrs.field))
def custom_define(f):
return attrs.define(f)Types
attrs offers two ways of attaching type information to attributes:
- {pep}
526annotations, - and the type argument to {func}
attr.ib/ {func}attrs.field.
This information is available to you:
>>> from attrs import define, field, fields
>>> @define
... class C:
... x: int = field()
... y = field(type=str)
>>> fields(C).x.type
<class 'int'>
>>> fields(C).y.type
<class 'str'>Currently, attrs doesn't do anything with this information but it's very useful if you'd like to write your own validators or serializers!
Originally, we didn't add the type argument to the new {func}attrs.field API, because type annotations are the preferred way.
But we reintroduced it later, so field can be used with the {func}attrs.make_class function.
We strongly discourage the use of the type parameter outside of {func}attrs.make_class.
(extending-metadata)=
Metadata
If you're the author of a third-party library with attrs integration, you may want to take advantage of attribute metadata.
Here are some tips for effective use of metadata:
Try making your metadata keys and values immutable.
This keeps the entire {class}~attrs.Attributeinstances immutable too.To avoid metadata key collisions, consider exposing your metadata keys from your modules.:
pythonfrom mylib import MY_METADATA_KEY @define class C: x = field(metadata={MY_METADATA_KEY: 1})Metadata should be composable, so consider supporting this approach even if you decide implementing your metadata in one of the following ways.
Expose
fieldwrappers for your specific metadata.
This is a more graceful approach if your users don't require metadata from other libraries.{doctest}>>> from attrs import fields, NOTHING >>> MY_TYPE_METADATA = '__my_type_metadata' >>> >>> def typed( ... cls, default=NOTHING, validator=None, repr=True, ... eq=True, order=None, hash=None, init=True, metadata=None, ... converter=None ... ): ... metadata = metadata or {} ... metadata[MY_TYPE_METADATA] = cls ... return field( ... default=default, validator=validator, repr=repr, ... eq=eq, order=order, hash=hash, init=init, ... metadata=metadata, converter=converter ... ) >>> >>> @define ... class C: ... x: int = typed(int, default=1, init=False) >>> fields(C).x.metadata[MY_TYPE_METADATA] <class 'int'>
(transform-fields)=
Automatic Field Transformation and Modification
attrs allows you to automatically modify or transform the class' fields while the class is being created.
You do this by passing a field_transformer hook to {func}~attrs.define (and friends).
Its main purpose is to automatically add converters to attributes based on their type to aid the development of API clients and other typed data loaders.
This hook must have the following signature:
.. function:: your_hook(cls: type, fields: list[attrs.Attribute]) -> list[attrs.Attribute]
:noindex:- cls is your class right before it is being converted into an attrs class.
This means it does not yet have the__attrs_attrs__attribute. - fields is a list of all
attrs.Attributeinstances that will later be set to__attrs_attrs__.
You can modify these attributes any way you want:
You can add converters, change types, and even remove attributes completely or create new ones!
For example, let's assume that you really don't like floats:
>>> def drop_floats(cls, fields):
... return [f for f in fields if f.type not in {float, 'float'}]
...
>>> @frozen(field_transformer=drop_floats)
... class Data:
... a: int
... b: float
... c: str
...
>>> Data(42, "spam")
Data(a=42, c='spam')A more realistic example would be to automatically convert data that you, for example, load from JSON:
>>> from datetime import datetime
>>>
>>> def auto_convert(cls, fields):
... results = []
... for field in fields:
... if field.converter is not None:
... results.append(field)
... continue
... if field.type in {datetime, 'datetime'}:
... converter = (lambda d: datetime.fromisoformat(d) if isinstance(d, str) else d)
... else:
... converter = None
... results.append(field.evolve(converter=converter))
... return results
...
>>> @frozen(field_transformer=auto_convert)
... class Data:
... a: int
... b: str
... c: datetime
...
>>> from_json = {"a": 3, "b": "spam", "c": "2020-05-04T13:37:00"}
>>> Data(**from_json) # ****
Data(a=3, b='spam', c=datetime.datetime(2020, 5, 4, 13, 37))Or, perhaps you would prefer to generate dataclass-compatible __init__ signatures via a default field alias.
Note, field_transformer receives {class}attrs.Attribute instances with default aliases already resolved, so the leading-underscore stripping has already been applied.
You can use the attrs.Attribute.alias_is_default flag to detect whether an alias was explicitly provided by the user or auto-generated.
>>> def dataclass_names(cls, fields):
... return [
... field.evolve(alias=field.name)
... if field.alias_is_default
... else field
... for field in fields
... ]
...
>>> @frozen(field_transformer=dataclass_names)
... class Data:
... public: int
... _private: str
... explicit: str = field(alias="aliased_name")
...
>>> Data(public=42, _private="spam", aliased_name="yes")
Data(public=42, _private='spam', explicit='yes')Customize Value Serialization in `asdict()`
attrs allows you to serialize instances of attrs classes to dicts using the {func}attrs.asdict function.
However, the result can not always be serialized since most data types will remain as they are:
>>> import json
>>> import datetime
>>> from attrs import asdict
>>>
>>> @frozen
... class Data:
... dt: datetime.datetime
...
>>> data = asdict(Data(datetime.datetime(2020, 5, 4, 13, 37)))
>>> data
{'dt': datetime.datetime(2020, 5, 4, 13, 37)}
>>> json.dumps(data)
Traceback (most recent call last):
...
TypeError: Object of type datetime is not JSON serializableTo help you with this, {func}~attrs.asdict allows you to pass a value_serializer hook.
It has the signature
.. function:: your_hook(inst: type, field: attrs.Attribute, value: typing.Any) -> typing.Any
:noindex:>>> from attr import asdict
>>> def serialize(inst, field, value):
... if isinstance(value, datetime.datetime):
... return value.isoformat()
... return value
...
>>> data = asdict(
... Data(datetime.datetime(2020, 5, 4, 13, 37)),
... value_serializer=serialize,
... )
>>> data
{'dt': '2020-05-04T13:37:00'}
>>> json.dumps(data)
'{"dt": "2020-05-04T13:37:00"}'File: docs/glossary.md
Glossary
:::{glossary}
dunder methods
"Dunder" is a contraction of "double underscore".
It's methods like __init__ or __eq__ that are sometimes also called magic methods or it's said that they implement an object protocol.
In spoken form, you'd call __init__ just "dunder init".
Its first documented use is a mailing list posting by Mark Jackson from 2002.
dict classes
A regular class whose attributes are stored in the {attr}object.__dict__ attribute of every single instance.
This is quite wasteful especially for objects with very few data attributes and the space consumption can become significant when creating large numbers of instances.
This is the type of class you get by default both with and without attrs (except with the next APIs {func}attrs.define(), attrs.mutable(), and attrs.frozen()).
slotted classes
A class whose instances have no {attr}object.__dict__ attribute and define their attributes in a object.__slots__ attribute instead.
In attrs, they are created by passing slots=True to @attr.s (and are on by default in {func}attrs.define(), attrs.mutable(), and attrs.frozen()).
Their main advantage is that they use less memory on CPython[^pypy] and are slightly faster.
However, they also come with several possibly surprising gotchas:
Slotted classes don't allow for any other attribute to be set except for those defined in one of the class' hierarchies
__slots__:{doctest}>>> from attr import define >>> @define ... class Coordinates: ... x: int ... y: int ... >>> c = Coordinates(x=1, y=2) >>> c.z = 3 Traceback (most recent call last): ... AttributeError: 'Coordinates' object has no attribute 'z'Slotted classes can inherit from other classes just like non-slotted classes, but some of the benefits of slotted classes are lost if you do that.
If you must inherit from other classes, try to inherit only from other slotted classes.However, it's not possible to inherit from more than one class that has attributes in
__slots__(you will get anTypeError: multiple bases have instance lay-out conflict).It's not possible to monkeypatch methods on slotted classes.
This can feel limiting in test code, however the need to monkeypatch your own classes is usually a design smell.If you really need to monkeypatch an instance in your tests, but don't want to give up on the advantages of slotted classes in production code, you can always subclass a slotted class as a dict class with no further changes and all the limitations go away:
{doctest}>>> import unittest.mock >>> @define ... class Slotted: ... x: int ... ... def method(self): ... return self.x >>> s = Slotted(42) >>> s.method() 42 >>> with unittest.mock.patch.object(s, "method", return_value=23): ... pass Traceback (most recent call last): ... AttributeError: 'Slotted' object attribute 'method' is read-only >>> @define(slots=False) ... class Dicted(Slotted): ... pass >>> d = Dicted(42) >>> d.method() 42 >>> with unittest.mock.patch.object(d, "method", return_value=23): ... assert 23 == d.method()Slotted classes must implement {meth}
__getstate__ <object.__getstate__>and {meth}__setstate__ <object.__setstate__>to be serializable with {mod}pickleprotocol 0 and 1.
Therefore, attrs creates these methods automatically for slotted classes.:::{note}
When decorating with@attr.s(slots=True)and the class already implements the {meth}__getstate__ <object.__getstate__>and {meth}__setstate__ <object.__setstate__>methods, they will be overwritten by attrs autogenerated implementation by default.This can be avoided by setting
@attr.s(getstate_setstate=False)or by setting@attr.s(auto_detect=True).{func}
~attrs.definesetsauto_detect=Trueby default.
:::Also, think twice before using {mod}
pickle.Slotted classes are weak-referenceable by default.
This can be disabled in CPython by passingweakref_slot=Falseto@attr.s[^pypyweakref].Since it's currently impossible to make a class slotted after it's been created, attrs has to replace your class with a new one.
While it tries to do that as graciously as possible, certain metaclass features like {meth}object.__init_subclass__do not work with slotted classes.The {attr}
type.__subclasses__attribute needs a garbage collection run (which can be manually triggered using {func}gc.collect), for the original class to be removed.
See issue #407 for more details.Pickling of slotted classes will fail if you define a class with missing attributes.
This situation can occur if you define an
attrs.field(init=False)and don't set the attribute by hand before pickling.You can't call a bare
super()in a decorated method likecontextlib.contextmanager.
Usesuper(YourClass, self)until we get a way to work around this from Python.
See also #1038 for more details.
field
As the project name suggests, attrs is all about attributes.
We especially tried to emphasize that we only care about attributes and not about the classes themselves -- because we believe the class belongs to the user.
This explains why the traditional API uses an {func}attr.ib (or attrib) function to define attributes and we still use the term throughout the documentation.
However, with the emergence of {mod}dataclasses, Pydantic, and other libraries, the term "field" has become a common term for a predefined attribute on a class in the Python ecosystem.
So with our new APIs, we've embraced it too by calling the function to create them {func}attrs.field, and use the term "field" throughout the documentation interchangeably.
See also {doc}names.
attribute
See {term}field.
:::
[^pypy]: On PyPy, there is no memory advantage in using slotted classes.
[^pypyweakref]: On PyPy, slotted classes are naturally weak-referenceable so weakref_slot=False has no effect.
File: docs/hashing.md
Hashing
Hash Method Generation
:::{warning}
The overarching theme is to never set the @attrs.define(unsafe_hash=X) parameter yourself.
Leave it at None which means that attrs will do the right thing for you, depending on the other parameters:
- If you want to make objects hashable by value: use
@define(frozen=True). - If you want hashing and equality by object identity: use
@define(eq=False)
Setting unsafe_hash yourself can have unexpected consequences so we recommend to tinker with it only if you know exactly what you're doing.
:::
Under certain circumstances, it's necessary for objects to be hashable.
For example if you want to put them into a {class}set or if you want to use them as keys in a {class}dict.
The hash of an object is an integer that represents the contents of an object.
It can be obtained by calling {func}hash on an object and is implemented by writing a __hash__ method for your class.
attrs will happily write a __hash__ method for you [^fn1], however it will not do so by default.
Because according to the definition from the official Python docs, the returned hash has to fulfill certain constraints:
[^fn1]: The hash is computed by hashing a tuple that consists of a unique id for the class plus all attribute values.
Two objects that are equal, must have the same hash.
This means that ifx == y, it must follow thathash(x) == hash(y).By default, Python classes are compared and hashed by their
id.
That means that every instance of a class has a different hash, no matter what attributes it carries.It follows that the moment you (or attrs) change the way equality is handled by implementing
__eq__which is based on attribute values, this constraint is broken.
For that reason Python 3 will make a class that has customized equality unhashable.
Python 2 on the other hand will happily let you shoot your foot off.
Unfortunately, attrs still mimics (otherwise unsupported) Python 2's behavior for backward-compatibility reasons if you setunsafe_hash=False.The correct way to achieve hashing by id is to set
@define(eq=False).
Setting@define(unsafe_hash=False)(which implieseq=True) is almost certainly a bug.:::{warning}
Be careful when subclassing!
Settingeq=Falseon a class whose base class has a non-default__hash__method will not make attrs remove that__hash__for you.It is part of attrs's philosophy to only add to classes so you have the freedom to customize your classes as you wish.
So if you want to get rid of methods, you'll have to do it by hand.The easiest way to reset
__hash__on a class is adding__hash__ = object.__hash__in the class body.
:::If two objects are not equal, their hash should be different.
While this isn't a requirement from a standpoint of correctness, sets and dicts become less effective if there are a lot of identical hashes.
The worst case is when all objects have the same hash which turns a set into a list.The hash of an object must not change.
If you create a class with
@define(frozen=True)this is fulfilled by definition, therefore attrs will write a__hash__function for you automatically.
You can also force it to write one withunsafe_hash=Truebut then it's your responsibility to make sure that the object is not mutated.This point is the reason why mutable structures like lists, dictionaries, or sets aren't hashable while immutable ones like tuples or
frozensets are:
point 1 and 2 require that the hash changes with the contents but point 3 forbids it.
For a more thorough explanation of this topic, please refer to this blog post: Python Hashes and Equality.
:::{note}
Please note that the unsafe_hash argument's original name was hash but was changed to conform with {pep}681 in 22.2.0.
The old argument name is still around and will not be removed -- but setting unsafe_hash takes precedence over hash.
The field-level argument is still called hash and will remain so.
:::
Hashing and Mutability
Changing any field involved in hash code computation after the first call to __hash__ (typically this would be after its insertion into a hash-based collection) can result in silent bugs.
Therefore, it is strongly recommended that hashable classes be frozen.
Beware, however, that this is not a complete guarantee of safety:
if a field points to an object and that object is mutated, the hash code may change, but frozen will not protect you.
Hash Code Caching
Some objects have hash codes which are expensive to compute.
If such objects are to be stored in hash-based collections, it can be useful to compute the hash codes only once and then store the result on the object to make future hash code requests fast.
To enable caching of hash codes, pass @define(cache_hash=True).
This may only be done if attrs is already generating a hash function for the object.
File: docs/how-does-it-work.md
(how)=
How Does It Work?
Boilerplate
attrs isn't the first library that aims to simplify class definition in Python.
But its declarative approach combined with no runtime overhead lets it stand out.
Once you apply the @attrs.define (or @attr.s) decorator to a class, attrs searches the class object for type hints and instances of attrs.fields.
Internally fields are a representation of the data passed, along with a counter to preserve the order of the attributes.
In order to ensure that subclassing works as you'd expect it to work, attrs also walks the class hierarchy and collects the attributes of all base classes.
Please note that attrs does not call super() ever.
It will write {term}dunder methods to work on all of those attributes which also has performance benefits due to fewer function calls.
Once attrs knows what attributes it has to work on, it writes the requested {term}dunder methods and -- depending on whether you wish to have a {term}dict <dict classes> or {term}slotted <slotted classes> class -- creates a new class for you (slots=True) or attaches them to the original class (slots=False).
While creating new classes is more elegant, we've run into several edge cases surrounding metaclasses that make it impossible to go this route unconditionally.
To be very clear: if you define a class with a single attribute without a default value, the generated __init__ will look exactly how you'd expect:
>>> import inspect
>>> from attrs import define
>>> @define
... class C:
... x: int
>>> print(inspect.getsource(C.__init__))
def __init__(self, x):
self.x = xNo magic, no meta programming, no expensive introspection at runtime.
Everything until this point happens exactly once when the class is defined.
As soon as a class is done, it's done.
And it's just a regular Python class like any other, except for a single __attrs_attrs__ attribute that attrs uses internally.
Much of the information is accessible via {func}attrs.inspect, {func}attrs.fields and other functions which can be used for introspection or for writing your own tools and decorators on top of attrs (like {func}attrs.asdict).
And once you start instantiating your classes, attrs is out of your way completely.
This static approach was very much a design goal of attrs and what I strongly believe makes it distinct.
(how-frozen)=
Immutability
In order to give you immutability, attrs will attach a __setattr__ method to your class that raises an {class}attrs.exceptions.FrozenInstanceError whenever anyone tries to set an attribute.
The same is true if you choose to freeze individual attributes using the {obj}attrs.setters.frozen on_setattr hook -- except that the exception becomes {class}attrs.exceptions.FrozenAttributeError.
Both exceptions subclass {class}attrs.exceptions.FrozenError.
Depending on whether a class is a dict class or a slotted class, attrs uses a different technique to circumvent that limitation in the __init__ method.
Once constructed, frozen instances don't differ in any way from regular ones except that you cannot change its attributes.
Dict Classes
Dict classes -- that is: regular classes -- simply assign the value directly into the class' eponymous __dict__ (and there's nothing we can do to stop the user to do the same).
The performance impact is negligible.
Slotted Classes
Slotted classes are more complicated.
Here it uses (an aggressively cached) {meth}object.__setattr__ to set your attributes.
This is (still) slower than a plain assignment:
$ pyperf timeit --rigorous \
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True)" \
"C(1, 2, 3)"
.........................................
Mean +- std dev: 228 ns +- 18 ns
$ pyperf timeit --rigorous \
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \
"C(1, 2, 3)"
.........................................
Mean +- std dev: 425 ns +- 16 nsSo on a laptop computer the difference is about 200 nanoseconds (1 second is 1,000,000,000 nanoseconds).
It's certainly something you'll feel in a hot loop but shouldn't matter in normal code.
Pick what's more important to you.
Summary
You should avoid instantiating lots of frozen slotted classes (meaning: @frozen) in performance-critical code.
Frozen dict classes have barely a performance impact, unfrozen slotted classes are even faster than unfrozen dict classes (meaning: regular classes).
(how-slotted-cached_property)=
Cached Properties on Slotted Classes
By default, the standard library {func}functools.cached_property decorator does not work on slotted classes, because it requires a __dict__ to store the cached value.
This could be surprising when using attrs, as slotted classes are the default.
Therefore, attrs converts cached_property-decorated methods when constructing slotted classes.
Getting this working is achieved by:
- Adding names to
__slots__for the wrapped methods. - Adding a
__getattr__method to set values on the wrapped methods.
For most users, this should mean that it works transparently.
:::{note}
The implementation does not guarantee that the wrapped method is called only once in multi-threaded usage.
This matches the implementation of cached_property in Python 3.12.
:::
File: docs/index.md
*attrs*: Classes Without Boilerplate
Release {sub-ref}release (What's new?)
:start-after: 'teaser-begin -->'
:end-before: '
[](https://www.variomedia.de/)
[](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek)
[](https://kraken.tech/)
[](https://privacy-solutions.org/)
[](https://klaviyo.com/)
[](https://filepreviews.io/)
[](https://www.testmuai.com/?utm_medium=sponsor&utm_source=structlog)
[](https://polar.sh/)
```{include} ../README.md
:start-after: 'sponsor-break-end -->'
:end-before: '<!-- teaser-end'/* Detailed source-code truncated for AI context efficiency. */
:start-after: '### *attrs* for Enterprise'Full Table of Contents
:maxdepth: 2
:caption: Getting Started
overview
why
examples:maxdepth: 2
:caption: Guides
types
init
comparison
hashing:maxdepth: 2
:caption: Reference
api
api-attr
glossary:maxdepth: 2
:caption: Advanced
extending
how-does-it-work:caption: Meta
:maxdepth: 1
names
license
changelog
PyPI <https://pypi.org/project/attrs/>
GitHub <https://github.com/python-attrs/attrs>
Third-party Extensions <https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs>
Contributing <https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md>
Funding <https://hynek.me/say-thanks/>