mrkeyoor.com_
Wed 05 Aug 19:55 UTC
PyPIUtilsupdated 05 Aug 2026

attrs

attrs writes your Python class boilerplate for you. You decorate a class with @define, declare attributes with type annotations, and attrs generates __init__, __repr__, equality methods, hashing, and more at class-creation time, so there is no runtime penalty per instance. It predates and inspired the standard library's dataclasses, and goes further: validators and converters that run on init and on assignment, frozen and slotted classes by default, evolve() for copies with changes, and hooks into almost every step of initialization. NASA has used it on Mars mission software.

Verdict

The best pure class-boilerplate generator in Python: faster to write, stricter, and more extensible than dataclasses, without pydantic's runtime weight. Pick it when your classes are internal objects with invariants; pick pydantic instead when your job is validating external data at the boundary.

API stability5/5The maintainers promise the classic @attr.s API stays importable indefinitely alongside the modern @define API; breaking changes are rare and CalVer releases (26.1.0) telegraph deprecations for years.
Docs5/5attrs.org has an unusual amount of honest writing: a why-not-dataclasses comparison, an essay explaining the dual API names, examples, and a full initialization-hooks reference.
Maintenance4/5Pushed August 2026 with 133 open issues (148 counting PRs); Hynek Schlawack has run it for over a decade with sponsor funding, though the bus factor is essentially one.
Ecosystem4/5Around 229 million weekly downloads because much of the ecosystem depends on it transitively; companions like cattrs and a third-party extension wiki exist, but the direct-use plugin world is smaller than pydantic's.

Use it if

  • You write many small data-holding classes and want __init__, __repr__, and __eq__ generated correctly without copy-paste dunder methods
  • You need validation or conversion on attribute assignment, not just at construction time; @define wires validators and converters into setattr by default
  • You want slotted classes for lower memory and attribute-typo protection without hand-writing __slots__ on every class
  • You outgrew dataclasses: attrs adds validators, converters, custom equality for things like NumPy arrays, and initialization hooks dataclasses does not have
Skip it if

Setup reality

pip install attrs; pure Python, no compiled parts, supports Python 3.9+. The setup cost is conceptual, not mechanical. There are two package names (attrs is the modern API, attr the classic one both are importable) and two decorator generations, so reading old code requires knowing @attr.s and @define are the same machinery. Mutable defaults must go through Factory or field(factory=...) or every instance shares one list. Slots-by-default in @define changes pickle and inheritance edge cases compared to plain classes. Type checkers understand attrs via a plugin baked into mypy and pyright support, which mostly just works.

Patterns

Define a class with generated init, repr, and eqdefine-basic-class

from attrs import define

@define
class Point:
    x: int
    y: int = 0

p = Point(1, 2)
print(p)            # Point(x=1, y=2)
print(p == Point(1, 2))  # True

@define is the modern API (attrs 20.1+). It creates a slotted class by default, so p.z = 3 raises AttributeError; pass slots=False if you need dynamic attributes.

Give an attribute a mutable default safelymutable-default-factory

from attrs import define, field, Factory

@define
class Bag:
    items: list[int] = Factory(list)
    # equivalent: items: list[int] = field(factory=list)

a, b = Bag(), Bag()
a.items.append(1)
print(b.items)  # [] , not shared

A bare items: list = [] would share one list across all instances (attrs raises an error for known mutable defaults to save you). Factory(list) calls list() per instance.

Validate values on init and on assignmentvalidators

from attrs import define, field, validators

@define
class Server:
    port: int = field(validator=[validators.instance_of(int),
                                 validators.ge(1), validators.le(65535)])

s = Server(8080)
s.port = 70000  # raises ValueError

With @define, validators run on __init__ and again on every attribute assignment. With the classic @attr.s they only run at init unless you set on_setattr yourself.

Write a custom validator functioncustom-validator

from attrs import define, field

def _non_empty(instance, attribute, value):
    if not value.strip():
        raise ValueError(f"{attribute.name} must not be blank")

@define
class User:
    name: str = field(validator=_non_empty)

The signature is fixed: (instance, attribute, value). Raise to reject; the return value is ignored. The @name.validator decorator form does the same inline.

Convert values automaticallyconverters

from attrs import define, field

@define
class Config:
    port: int = field(converter=int)
    tags: tuple = field(converter=tuple, factory=tuple)

c = Config(port="8080", tags=["a", "b"])
print(c.port, c.tags)  # 8080 ('a', 'b')

Converters run before validators, on init and (with @define) on assignment. They receive only the value, so annotate the field with the converted type, not the input type.

Make an immutable classfrozen-class

from attrs import frozen, evolve

@frozen
class Coordinates:
    lat: float
    lon: float

home = Coordinates(52.5, 13.4)
nearby = evolve(home, lon=13.5)
# home.lat = 0  -> raises FrozenInstanceError

@frozen is @define(frozen=True). Frozen instances are hashable by default and cost a bit extra at init; evolve() is the intended way to get modified copies.

Turn instances into dicts and tuplesasdict-serialization

from attrs import asdict, astuple, define

@define
class Point:
    x: int
    y: int

print(asdict(Point(1, 2)))   # {'x': 1, 'y': 2}
print(astuple(Point(1, 2)))  # (1, 2)
print(asdict(Point(1, 2), filter=lambda a, v: a.name != 'y'))

asdict recurses into nested attrs classes, lists, and dicts. For real serialization work (JSON with unions, datetimes, renaming) use cattrs instead of hand-rolling on asdict.

Run logic after the generated initpost-init-hook

from attrs import define, field

@define
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)

    def __attrs_post_init__(self):
        self.area = self.width * self.height

field(init=False) keeps derived values out of the constructor signature. On frozen classes, set derived fields inside __attrs_post_init__ via object.__setattr__.

Use underscore attributes with clean init namesprivate-attribute-alias

from attrs import define

@define
class Connection:
    _socket: object

# init strips the underscore:
conn = Connection(socket=my_socket)

Leading underscores are removed from the __init__ argument name automatically; pass alias=... to field() if you want a different public name.

Define attributes without type hintsno-type-annotations

from attrs import define, field

@define
class SomeClass:
    a_number = field(default=42)
    list_of_numbers = field(factory=list)

Types are optional: assigning field() marks the attribute. Do not mix annotated and bare-field styles in one class; attrs requires one style per class.

Read the classic attr.s API in old codecompare-classic-api

import attr

@attr.s
class LegacyPoint:
    x = attr.ib(default=0)
    y = attr.ib(default=0)

# same machinery as:
# from attrs import define
# @define
# class Point: x: int = 0; ...

@attr.s does not enable slots or setattr validators by default, unlike @define. Both APIs stay supported, but new code should use attrs.define per the project docs.

Control equality and ordering generationequality-and-ordering

from attrs import define, field

@define(order=True)
class Version:
    major: int
    minor: int
    build_meta: str = field(eq=False, order=False, default="")

print(Version(1, 2) < Version(1, 3))  # True

@define generates eq but not ordering unless order=True. Per-field eq=False excludes noisy fields (timestamps, metadata) from comparisons and hashing.

Alternatives

PackageRegistryPick it when
pydanticPyPIYour data comes from outside (JSON, env, forms) and you want deep validation plus serialization built in, accepting the heavier runtime.
msgspecPyPIYou want maximum speed for (de)serializing typed structs to JSON or MessagePack; its Struct classes are far faster but less flexible.
dataclassesPyPIYou just want stdlib dataclasses on modern Python: import dataclasses with no install at all (this PyPI entry is only the old 3.6 backport).
cattrsPyPIYou are already on attrs and need structuring and unstructuring to JSON-ready dicts; it is the companion, not a replacement.