Extending

Each attrs-decorated class has a __attrs_attrs__ class attribute. It is a tuple of attr.Attribute carrying meta-data about each attribute.

So it is fairly simple to build your own decorators on top of attrs:

>>> import attr
>>> def print_attrs(cls):
...     print(cls.__attrs_attrs__)
...     return cls
>>> @print_attrs
... @attr.s
... class C(object):
...     a = attr.ib()
(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None),)

Warning

The 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():
   pass

is 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.

Unfortunately, this 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 MyPlugin

Then tell mypy about your plugin using your project’s mypy.ini:

[mypy]
plugins=<path to file>

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.

Types

attrs offers two ways of attaching type information to attributes:

  • PEP 526 annotations on Python 3.6 and later,

  • and the type argument to attr.ib.

This information is available to you:

>>> import attr
>>> @attr.s
... class C(object):
...     x: int = attr.ib()
...     y = attr.ib(type=str)
>>> attr.fields(C).x.type
<class 'int'>
>>> attr.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!

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 Attribute instances immutable too.

  • To avoid metadata key collisions, consider exposing your metadata keys from your modules.:

    from mylib import MY_METADATA_KEY
    
    @attr.s
    class C(object):
      x = attr.ib(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 attr.ib wrappers for your specific metadata. This is a more graceful approach if your users don’t require metadata from other libraries.

    >>> MY_TYPE_METADATA = '__my_type_metadata'
    >>>
    >>> def typed(
    ...     cls, default=attr.NOTHING, validator=None, repr=True,
    ...     eq=True, order=None, hash=None, init=True, metadata={},
    ...     type=None, converter=None
    ... ):
    ...     metadata = dict() if not metadata else metadata
    ...     metadata[MY_TYPE_METADATA] = cls
    ...     return attr.ib(
    ...         default=default, validator=validator, repr=repr,
    ...         eq=eq, order=order, hash=hash, init=init,
    ...         metadata=metadata, type=type, converter=converter
    ...     )
    >>>
    >>> @attr.s
    ... class C(object):
    ...     x = typed(int, default=1, init=False)
    >>> attr.fields(C).x.metadata[MY_TYPE_METADATA]
    <class 'int'>