website/blog/2026-07-01-pyrefly-attrs.md
Built-in support for attrs is available in Pyrefly as of version 1.2.0-dev.1, and ships in the upcoming 1.2.0 stable release. You get accurate constructor signatures and better type safety for your attrs classes, with no plugins or configuration to set up.
<!-- truncate -->attrs consists of two separate APIs with quite different semantics: a classic
one under the attr namespace and a modern one under attrs. While
PEP 681's dataclass_transform provides a
useful baseline for type checkers to understand parts of attrs, many features
cannot be expressed in terms of standard type annotations.
Historically, attrs users that want type checking have either had to use Mypy
(which implements dedicated attrs support via a plugin), limit themselves to a
subset of the API compatible with dataclass_transform, or just live with
limited type checking support.
At the time of writing, Pyrefly and Mypy are the only type checkers that provide full support for attrs.
attrs lets you define a class by declaring its attributes, then generates the
boilerplate for you: __init__, __repr__, __eq__, ordering, hashing,
slots, and immutability. If you've used Python's built-in dataclasses, this
will feel familiar. attrs came first, and dataclasses was directly inspired by
it. attrs remains the more flexible of the two.
For example:
from attrs import define, field
@define
class Tune:
title: str
key: str
choruses: int = field(default=1)
bluebossa = Tune(title="Blue Bossa", key="Cm")
print(bluebossa)
With that single decorator, attrs writes a typed __init__, a readable
__repr__, and value-based equality for you. If you pass the wrong thing, say
Tune(title="Blue Bossa", key=5), that's a bug you'd like to hear about before
you run the code.
So how does Pyrefly work with attrs? Here's what the support covers:
@define, @frozen, @mutable, attrs.field) and the classic
API (@attr.s, @attr.ib, @attr.dataclass), across both the attr. and
attrs. namespaces. No import errors or stray red squiggles because your tools
don't understand what attrs is.For the full list of supported features, check out the documentation.
This is where attrs differs from most libraries a type checker has to support. attrs has spent ten years accumulating two complete APIs:
@attr.s on the class, attr.ib() on each attribute.@attrs.define and
attrs.field().Both are fully supported by attrs, and since the classic style was never
deprecated, the same codebase can end up containing both, even though mixing
them is generally discouraged. The catch is that they don't behave identically.
The clearest example is how each one decides which lines in your class body are
actually fields. The modern
decorators read your annotations. The classic @attr.s ignores bare annotations
by default and only collects attr.ib() assignments, unless you opt in with
auto_attribs=True:
import attr
@attr.s
class Horn:
name: str # NOT a field, just an annotation, under classic @attr.s
serial = attr.ib() # this is the only real field
Horn(serial="A-440") # Pyrefly accepts this
Horn("Bach Strad", "A-440") # Pyrefly reports an error, just like attrs would
Pyrefly reads how you wrote the class and adapts, the same way it reads a
Pydantic model's strict or extra settings. Because it resolves this per
class, a base in one style and a subclass in the other compose exactly as they
do at runtime.
The twist hiding in attrs is that the constructor doesn't always look like the
class. Add a converter to a field and the __init__ parameter takes the
converter's input type, while the attribute keeps its output type, and
Pyrefly tracks both:
from attrs import define, field
def to_bpm(s: str) -> int:
return int(s.removesuffix(" bpm"))
@define
class Chart:
tempo: int = field(converter=to_bpm)
medium_swing = Chart("120 bpm") # takes a str, that's what to_bpm reads
reveal_type(medium_swing.tempo) # int, what to_bpm hands back
Chart("120 bpm") type-checks because the converter accepts a str, while
medium_swing.tempo is an int everywhere you read it. That's the kind of
detail you'd otherwise only discover at runtime.
There are no special configurations or plugins required to start using Pyrefly with attrs:
Pyrefly does not require attrs-specific configuration and recognizes both the classic and modern APIs when available.
If you'd like to play with some examples, we put together a demo repo you can clone and try out.
Try attrs support in your own projects and let us know what works and what doesn't. You can open an issue on GitHub or connect with us on Discord.
attrs joins Pydantic and Django in the set of libraries Pyrefly supports without a plugin. If there's a particular package you rely on, let us know.