Define less, check more: Pyrefly now speaks attrs | Pyrefly
Skip to main content<br>Built-in support for attrs is available in Pyrefly as<br>of version 1.2.0-dev.1, and ships in the upcoming 1.2.0 stable release. You get<br>accurate constructor signatures and better type safety for your attrs classes,<br>with no plugins or configuration to set up.
attrs consists of two separate APIs with quite different semantics: a classic<br>one under the attr namespace and a modern one under attrs. While<br>PEP 681's dataclass_transform provides a<br>useful baseline for type checkers to understand parts of attrs, many features<br>cannot be expressed in terms of standard type annotations.
Historically, attrs users that want type checking have either had to use Mypy<br>(which implements dedicated attrs support via a plugin), limit themselves to a<br>subset of the API compatible with dataclass_transform, or just live with<br>limited type checking support.
At the time of writing, Pyrefly and Mypy are the only type checkers that provide<br>full support for attrs.
What attrs generates for you
attrs lets you define a class by declaring its attributes, then generates the<br>boilerplate for you: __init__, __repr__, __eq__, ordering, hashing,<br>slots, and immutability. If you've used Python's built-in dataclasses, this<br>will feel familiar. attrs came first, and dataclasses was directly inspired by<br>it. attrs remains the more flexible of the two.
For example:
from attrs import define, field
@define<br>class Tune:<br>title: str<br>key: str<br>choruses: int = field(default=1)
bluebossa = Tune(title="Blue Bossa", key="Cm")<br>print(bluebossa)
With that single decorator, attrs writes a typed __init__, a readable<br>__repr__, and value-based equality for you. If you pass the wrong thing, say<br>Tune(title="Blue Bossa", key=5), that's a bug you'd like to hear about before<br>you run the code.
Pyrefly & attrs: How it works
So how does Pyrefly work with attrs? Here's what the support covers:
Understands the core decorators and field specifiers : Pyrefly recognizes<br>the modern API (@define, @frozen, @mutable, attrs.field) and the classic<br>API (@attr.s, @attr.ib, @attr.dataclass), across both the attr. and<br>attrs. namespaces. No import errors or stray red squiggles because your tools<br>don't understand what attrs is.
No plugin required : Pyrefly recognizes attrs classes out of the box. Unlike<br>Mypy, it needs no attrs plugin, and there's nothing to configure.
Static analysis that reflects runtime logic : much of attrs' behavior is<br>decided when your class is created, including which assignments become<br>fields, what the constructor looks like, and whether the class is ordered or<br>frozen. Pyrefly's analysis mirrors those runtime rules as closely as possible,<br>so what you see in your IDE matches what happens when the code runs.
Catches attrs misconfigurations : many ways of misusing attrs don't fail<br>until the class is built at runtime. Pyrefly understands attrs' own rules for<br>how a valid class is put together, so it surfaces those mistakes statically, as<br>you type, instead of leaving them to blow up later.
For the full list of supported features, check out the<br>documentation.
Automatic recognition: classic, modern, or both
This is where attrs differs from most libraries a type checker has to support.<br>attrs has spent ten years accumulating two complete APIs :
The classic style, from attrs 15.0.0 (2015), attrs's first release:<br>@attr.s on the class, attr.ib() on each attribute.
The modern style, added in attrs 20.1.0 (2020): @attrs.define and<br>attrs.field().
Both are fully supported by attrs, and since the classic style was never<br>deprecated, the same codebase can end up containing both, even though mixing<br>them is generally discouraged. The catch is that they don't behave identically.<br>The clearest example is how each one decides which lines in your class body are<br>actually fields. The modern<br>decorators read your annotations. The classic @attr.s ignores bare annotations<br>by default and only collects attr.ib() assignments, unless you opt in with<br>auto_attribs=True:
import attr
@attr.s<br>class Horn:<br>name: str # NOT a field, just an annotation, under classic @attr.s<br>serial = attr.ib() # this is the only real field
Horn(serial="A-440") # Pyrefly accepts this<br>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<br>Pydantic model's strict or extra settings. Because it resolves this per<br>class, a base in one style and a subclass in the other compose exactly as they<br>do at runtime.
The twist hiding in attrs is that the constructor doesn't always look like the<br>class. Add a converter to a field and the __init__ parameter takes the<br>converter's input type, while the attribute keeps its output type, and<br>Pyrefly tracks both:
from attrs import define, field
def to_bpm(s: str) -> int:<br>return int(s.removesuffix(" bpm"))
@define<br>class Chart:<br>tempo: int = field(converter=to_bpm)
medium_swing = Chart("120 bpm") # takes a...