attrs Documentation

repository·main·Indexed 26 days ago

https://github.com/python-attrs/attrs

A Python package that simplifies class writing by automatically implementing common object protocols like __init__, __repr__, and __eq__ to reduce boilerplate. It provides modern tools like @define and @frozen for declarative class definitions, as well as a legacy attr namespace. Features include dynamic class creation with make_class, attribute validation, converters, and utilities for converting instances to dictionaries or tuples.

Tokens
17.1K
Snippets
46
Records
122
Agent score
88%

What's inside attrs

  1. Define classes with attrs

    main
    Use attrs to create well-behaved Python classes by providing a class decorator and declaratively defining attributes. This approach automates the generation of boilerplate methods (dunder methods) while keeping the class a standard Python class that is indistinguishable from a regular class at runtime.
  2. How attrs works: Declarative class definition

    main

    When you apply the @attrs.define (or @attr.s) decorator to a class, attrs performs a one-time setup during class definition. It searches for type hints and attrs.field instances to determine the class attributes. It then writes the necessary dunder methods (like __init__) to handle these attributes.

    This process happens exactly once when the class is defined, meaning there is no runtime overhead or expensive introspection during class instantiation. Once a class is defined, it behaves like a regular Python class, and attrs stays out of the way.

    import inspect
    from attrs import define
    
    @define
    class C:
        x: int
    
    print(inspect.getsource(C.__init__))
    # Output:
    def __init__(self, x):
        self.x = x
  3. Use type annotations with attrs

    main

    You can use PEP 526 type annotations with attrs. However, if you choose to use type annotations, you must annotate all attributes in the class.

    Warning: If you define a class with an attrs.field that lacks a type annotation, attrs will ignore any other fields that have a type annotation but are not defined using attrs.field.

  4. Use modern Next-Generation (NG) APIs for new code

    main

    For new projects, it is recommended to use the modern 'Next-Generation' (NG) APIs. These APIs are more expressive and enable modern defaults like slots and type annotation awareness by default. They were introduced in attrs 20.1.0 and can be imported directly from the attrs namespace as of version 21.3.0.

    Key modern functions:

    • attrs.define(): Defines a new class.
    • attrs.mutable(): An alias for attrs.define().
    • attrs.frozen(): An alias for attrs.define(frozen=True).
    • attrs.field(): Defines an attribute within a class.
    from attrs import define
    
    @define
    class Point:
        x: int
        y: int
  5. Understand the attrs philosophy and design

    main

    Core Principles

    • Regular Classes: attrs is designed for creating full-featured classes with types, attributes, and methods, not just data containers.
    • User Ownership: attrs does not use metaclasses or inject unexpected classes into your inheritance tree. It simply attaches necessary dunder methods to your existing class definition.
    • Minimal API Impact: To avoid polluting your class namespace, most utility tools are provided as standalone functions that operate on instances, rather than being attached as methods.
    • Zero Runtime Overhead: All heavy lifting is performed at class definition time. Once the class is instantiated, attrs has no runtime impact.
    • Explicit over Implicit: The library avoids 'clever' magic or guessing intent, following the principle that explicit is better than implicit.
  6. Hook into initialization with pre-init and post-init

    main

    You can execute code at specific stages of the attrs initialization process using these hooks:

    • __attrs_pre_init__: Runs before attrs starts initializing. Primarily used to call super().__init__() when subclassing non-attrs classes.
    • __attrs_post_init__: Runs after attrs has finished initializing the instance. Useful for deriving attributes or performing cross-attribute validation.

    Note for @frozen classes: You cannot directly set attributes in __attrs_post_init__ on a frozen class. You must use object.__setattr__(self, "name", value) instead.

    Order of Execution:

    1. __attrs_pre_init__ (on current class)
    2. For each attribute: default factory $\rightarrow$ converter $\rightarrow$ validators
    3. __attrs_post_init__ (on current class)
    # Post-init example
    @define
    class C:
        x: int
        y: int = field(init=False)
        def __attrs_post_init__(self):
            self.y = self.x + 1
    
    # Post-init on a frozen class
    @frozen
    class Frozen:
        x: int
        y: int = field(init=False)
        def __attrs_post_init__(self):
            object.__setattr__(self, "y", self.x + 1)
  7. Create derived attributes

    main

    There are three common ways to create attributes that depend on other attributes:

    1. __attrs_post_init__ (Simplest): Set the derived attribute after initialization is complete.
    2. Decorator-based default: Use @attribute_name.default to define a factory method that accesses other attributes.
    3. Class method factory (Recommended): Use a @classmethod to handle complex setup. This is generally more testable and explicit.

    Note: If using the decorator approach, the derived field must be included in the __init__ (e.g., field()) unless you specifically want it excluded.

    # Approach 1: Post-init
    @define
    class APIClient:
        token: str
        client: WebClient = field(init=False)
    
        def __attrs_post_init__(self):
            self.client = WebClient(self.token)
    
    # Approach 2: Decorator-based default
    @define
    class APIClient:
        token: str
        client: WebClient = field() 
    
        @client.default
        def _client_factory(self):
            return WebClient(self.token)
    
    # Approach 3: Class method factory (Best practice)
    @define
    class APIClient:
        client: WebClient
    
        @classmethod
        def from_token(cls, token: str) -> "APIClient":
            return cls(client=WebClient(token))
  8. Configure Mypy for custom attrs decorators

    main

    If you wrap attrs decorators, Mypy's plugin may not recognize the resulting classes. To fix this, you can create a custom Mypy plugin that uses attr_attrib_makers, attr_class_makers, and attr_dataclass_makers to register your custom methods.

    Note: This only tells Mypy that a class is an attrs class; it cannot inform Mypy about changes to defaults like eq or order.

    from mypy.plugin import Plugin
    from mypy.plugins.attrs import (
       attr_attrib_makers,
       attr_class_makers,
       attr_dataclass_makers,
    )
    
    # Register your custom methods
    attr_dataclass_makers.add("my_module.method_looks_like_attr_dataclass")
    attr_class_makers.add("my_module.method_looks_like_attr_s")
    attr_attrib_makers.add("my_module.method_looks_like_attrib")
    
    class MyPlugin(Plugin):
        pass
    
    def plugin(version):
        return MyPlugin

    Then add the plugin to your mypy.ini:

    [mypy]
    plugins=<path to file>
  9. Handle version-specific logic with `attr.__version_info__`

    main

    To write backward-compatible code that avoids warnings on modern releases, use attr.__version_info__. This attribute behaves similarly to sys.version_info and allows you to check the version of attrs currently in use.

    import attr
    
    if getattr(attr, "__version_info__", (0,)) >= (19, 2):
        cmp_off = {"eq": False}
    else:
        cmp_off = {"cmp": False}
    
    @attr.s(**cmp_off)
    class C:
        pass
  10. Use slotted classes for memory efficiency

    main

    In attrs, slotted classes are created by passing slots=True to @attr.s. This option is enabled by default when using attrs.define(), attrs.mutable(), or attrs.frozen().

    Slotted classes use less memory (on CPython) and are slightly faster than standard 'dict classes' because they use __slots__ instead of __dict__.

    Important Gotchas:

    • No extra attributes: You cannot set attributes that aren't defined in the class hierarchy's __slots__.
    • Inheritance limits: You cannot inherit from more than one class that has __slots__ (results in TypeError: multiple bases have instance lay-out conflict).
    • No monkeypatching: You cannot monkeypatch methods on slotted classes. If you need to monkeypatch for testing, you can subclass the slotted class as a dict class by setting slots=False.
    from attr import define
    
    @define
    class Coordinates:
        x: int
        y: int
    
    c = Coordinates(x=1, y=2)
    # c.z = 3  # This would raise AttributeError
  11. Implement immutability with frozen classes

    main

    To make a class immutable, use the frozen=True parameter (or the @frozen decorator). attrs achieves this by attaching a __setattr__ method to the class that prevents attribute modification after instantiation.

    If an attempt is made to modify an attribute on a frozen class, attrs raises an attrs.exceptions.FrozenInstanceError. If you use attrs.setters.frozen on a specific attribute, attempting to modify that specific attribute will raise an attrs.exceptions.FrozenAttributeError. Both exceptions are subclasses of attrs.exceptions.FrozenError.