attrs review
attrs 26.1.0 turns declared Python fields into constructors, readable representations, equality methods, ordering, and hashes. Converters can normalize incoming values before validators check them, and modern @define classes validate later assignments too. Version 26.1.0 changes advanced class generation: aliases are resolved before field_transformer runs, and Attribute.alias_is_default tells a transformer whether an alias was generated or supplied. The same release fixes typing for tuples passed to validators.optional() and allows nested validators.disabled() contexts. This is object-model tooling, not a JSON parser; cattrs is the companion when values must cross a wire boundary.
attrs 26.1.0 installed as one 1 MB package in 0.2 seconds with zero audit findings in our sandbox, making it a cheap dependency for Python classes that need converters, validators, or slotted instances. Stay with dataclasses for plain records, and use Pydantic when parsing external payloads is the main task.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import attr in 0.14s · pure Python · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does attrs install cleanly?
Yes. In a fresh container with an empty cache, pip install attrs finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does attrs need to run?
Python >=3.9, and nothing compiled: it is pure Python. In our run import attr succeeded in 0.14s, and the package ships py.typed for type checkers.
attrs or pydantic: which should you use?
pydantic: Pick it for API or configuration data that needs coercion, nested errors, schemas, and serialization. attrs 26.1.0 installed as one 1 MB package in 0.2 seconds with zero audit findings in our sandbox, making it a cheap dependency for Python classes that need converters, validators, or slotted instances.
When should you not use attrs?
A plain record only needs init, repr, and equality; stdlib dataclasses handles that without installing a package
Use it if
- Internal models need generated methods plus converters or validators on individual fields
- A slotted class should reject misspelled attributes unless a particular integration requires __dict__
- A codebase still imports attr.s and must introduce the current attrs.define API without a flag-day migration
- Class construction needs pre-init, post-init, field transformers, or comparison rules beyond dataclasses
- A plain record only needs init, repr, and equality; stdlib dataclasses handles that without installing a package
- Untrusted request bodies need nested coercion, structured errors, and schema output; Pydantic owns that job directly
- asdict() is expected to round-trip unions, dates, renamed keys, and custom formats; attrs leaves those rules to cattrs or your serializer
- Runtime code attaches undeclared attributes or test doubles patch instances; @define uses slots unless slots=False is explicit
- The team cannot absorb different defaults between classic @attr.s and current @define while both spellings remain in the repository
Setup reality
Our attrs 26.1.0 install finished in 0.2 seconds on Python 3.12 and left one package using 1 MB. It is pure Python, declares zero direct dependencies, requires Python 3.9 or later, and includes py.typed. pip-audit reported zero known vulnerabilities. import attr succeeded in 0.14 seconds, confirming that the classic import remains usable. The installed metadata did not expose a license value we could report.
New code should import define and field from attrs. Existing code may still use attr.s and attr.ib, which the README says will remain available indefinitely. Those interfaces are backed by the same project but do not share every default. @define enables slots and assignment validation; classic @attr.s does not turn both on automatically. A migration must check behavior, not just rename imports.
Use field(factory=list) or Factory(list) for mutable defaults so each instance gets its own value. Converters execute before validators. With the current API they may run again on assignment, so a field stored as int can legitimately accept a string through converter=int. Tests and public constructor typing should account for that wider input.
Slots remove the normal instance dict, which breaks code that adds fields later and can complicate some mixins, pickling, weak references, and monkeypatching. Pass slots=False for those classes. A frozen instance must be copied with evolve(); a post-init hook that sets a derived field on a frozen class must call object.setattr. attrs needs no credentials or config file, but these decorator choices become runtime policy for every instance.
Patterns
Generate methods for a slotted class define-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 creates __init__, __repr__, and __eq__, with slots enabled. Set slots=False if callers must add attributes that were not declared.
Give each object its own list mutable-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 sharedattrs calls the factory during each construction, so changing one Bag does not mutate another Bag's default list.
Validate construction and later writes validators
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@define checks the initial port and subsequent assignments. The classic @attr.s decorator has different assignment-validation defaults.
Attach a custom field check custom-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 callback receives the object, its Attribute metadata, and the candidate value. It must raise to reject input because attrs ignores the return value.
Convert constructor input converters
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')Conversion happens before validation. The annotation describes the stored value, while the constructor may accept more types than that annotation suggests.
Replace one value on a frozen class frozen-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 FrozenInstanceErrorDirect writes raise FrozenInstanceError. evolve() constructs a replacement and therefore runs the declared converters and validators again.
Turn an attrs object into built-ins asdict-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() descends into nested attrs objects and containers. Key renaming, unions, dates, and reverse conversion call for cattrs or another serializer.
Fill a derived field after init post-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.heightinit=False keeps area out of the generated signature. A frozen class must write the derived value with object.__setattr__ inside this hook.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pydantic | PyPI | Pick it for API or configuration data that needs coercion, nested errors, schemas, and serialization. |
| msgspec | PyPI | Pick it when typed records mainly exist for fast JSON or MessagePack encoding and decoding. |
| dataclasses | PyPI | Use Python's built-in decorator for simple records; the PyPI release is only an old-version backport. |
| cattrs | PyPI | Add it to attrs when dictionaries must become typed objects and later turn back into wire-ready values. |
More utils guides
lru-cache · type-fest · ajv · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

