mrkeyoor.com_
Sat 19 Sept 23:48 UTC
PyPIUtilsupdated 19 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed attrsScreenshot of attrs documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport attr in 0.14s · pure Python · py.typed · requires Python >=3.9
Known vulns0(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

API stability5/5The README promises that attr.s, attr.ib, and the attr import will remain available indefinitely while attrs.define is the documented choice for new code. Release 26.1.0 does contain a narrow breaking change for field_transformer users because aliases now arrive resolved. Ordinary generated constructors and validators retain their established shape, but transformer-heavy code needs a release-note check.
Docs5/5The official site explains annotated and untyped declarations, initialization order, converters, validators, slots, frozen classes, hashing, comparison hooks, and both API generations. Its changelog names the exact 26.1.0 transformer break and links each item to an issue. The project also states where attrs stops and cattrs begins, which keeps asdict() examples from being mistaken for a complete serialization system.
Maintenance4/5PyPI reports 26.1.0, released on 2026-03-19, and GitHub recorded a push on 2026-08-25. The repository showed 150 open issues and pull requests. Current work includes compatibility fixes and type-annotation corrections while Python support starts at 3.9. The active repository and strict compatibility policy justify a high score, with one point held back for the sizable mixed issue and PR queue.
Ecosystem4/5The supplied weekly snapshot is roughly 228.8 million downloads, and the repository had 5,832 GitHub stars when checked. Classic and current imports coexist, py.typed ships in the package, and cattrs covers structuring and unstructuring. attrs is common infrastructure across Python dependency trees, although web frameworks tend to expose Pydantic models more directly at request and response boundaries.

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
Skip it if

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 shared

attrs 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 FrozenInstanceError

Direct 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.height

init=False keeps area out of the generated signature. A frozen class must write the derived value with object.__setattr__ inside this hook.

Alternatives

PackageRegistryPick it when
pydanticPyPIPick it for API or configuration data that needs coercion, nested errors, schemas, and serialization.
msgspecPyPIPick it when typed records mainly exist for fast JSON or MessagePack encoding and decoding.
dataclassesPyPIUse Python's built-in decorator for simple records; the PyPI release is only an old-version backport.
cattrsPyPIAdd 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.