An attempt at catharsis. This is a deeply personal blog post about the most influential project I’ve ever created: attrs, the progenitor of modern Python class utilities. I’m retelling its history from my perspective, how it begot dataclasses, and how I’m leading it into the future.
Remember how long you’ve been putting this off, how many extensions the gods gave you, and you didn’t use them.— Marcus Aurelius, Meditations
You might detect hints of bitterness in this post. That’s because it also deals with feelings, and there’s been a lot of those. You see, attrs and I have been subject to constant erasure and revisionism, bordering on abuse.
The origin of attrs is an early morning/late night (depending on the perspective) IRC session with my friend Glyph in January 2015. At that point, its predecessor characteristic1 has been out for a while and although it was very useful, I felt some frustrations. Most notably the verbose name that is annoying to type and the way attributes are defined (arguments to the decorator, instead in the class body):
from characteristic import attributes @attributes(["x", "y"]) class Point: pass
We had some really wild ideas I don’t even dare to reproduce after the amount of discomfort some people feel about the “cutesy”
attr.ib names2 that just mean “attr[ibutes]s” and “attrib[ute]” respectively (it included callable modules). If you look at the long, awkward names in the last example, you may have more sympathy for why we might have oversteered a wee bit in our quest to have short names in attrs.
It’s important to understand, though, that we thought of a much narrower scope for the project than what it is today. So while I had some discomfort from using
attr.Factory, I begrudgingly made peace with it, because I didn’t expect the module name to be used for more than
import attr @attr.s class Point: x = attr.ib() y = attr.ib()
Back when I tried to register
attr on PyPI, it turned out that there already is a project of that name3. While that was a constant annoyance and some people were really jerks about it4, now I’m glad things worked out the way they did.
Because today, it allows us to break backwards-compatibility without breaking anyone’s code.
After some time of community reluctancy about ostensible magic5, attrs took off in 2016, fueled by a blog post by Glyph titled The One Python Library Everyone Needs.
It’s hard to imagine today, but back then, if you wanted a class that carries some attributes, you had to either write walls of boilerplate code or employ hacks like subclassing namedtuples6:
class Point(namedtuple('Point', ['x', 'y'])): __slots__ = () @property def hypot(self): return (self.x ** 2 + self.y ** 2) ** 0.5 def __str__(self): return 'Point: x=%6.3f y=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot)
attrs changed that and suddenly creating a new class for your abstraction stopped being chore. This meant fewer God objects, fewer nondescript tuples, and fewer free-form dicts.
attrs became so good they couldn’t ignore it7.
And so in 2017 people started asking for something similar in the Python standard library, which lead to an email from Guido van Rossum to me, after I’ve announced attrs 17.1.0 and was on my way to PyCon US 20178. He asked me to meet up with him and Eric V. “f-strings” Smith to talk about “the future of attrs and how/whether we might provide some of its functionality in future stdlib versions”.
This lead to a long and productive meeting in the hallway of the Portland convention center (followed by several months of online cooperation) and became the inception of PEP 557, and thus the
dataclasses module in Python 3.79.
Types Go Mainstream
At that time, type hints in Python weren’t a topic that was a regular source of hot takes and flame wars on Twitter. I didn’t use them myself at all. But in hindsight it is clear that
dataclasses’s approach of using type hints for metadata that is used at runtime – that has been adopted by other class-building toolkits ever since – had a strong influence on the adoption of type hints, because they gave them an actual utility. Something IPv6 is still struggling with, 26 years in.
Thanks to amazing contributors, attrs embraced type hints early on too and shipped them before Python 3.7 with
dataclasses was released. My biggest gratitude goes to David Euresti who was the original author of the mypy attrs plugin and to this day hasn’t blocked me on GitHub despite my constant pestering.
But attrs typing support wouldn’t be even close to what it is without (in alphabetical order) Chad Dombrova, Ethan Smith, Ivan Levkivskyi, Jukka Lehtosalo, Łukasz Langa, and Tin Tvrtković who did most of the work and whom I still regularly pester when I’m lost. Thank you so much, everyone!
At this point you could use type annotations like this:
@attr.s(auto_attribs=True) class Point: x: int y: int
Or using the
@attr.dataclass easter egg that was never documented, but was immediately found and used. In other words: dataclasses were always a strict subset of attrs.
The Dark Times
How much better to heal than seek revenge from injury.— Seneca
Had I known what would follow after Python 3.7 was released, I would be much more careful about spending time and energy on making dataclasses happen.
It turns out that for quite a few people the release of dataclasses in the standard library of Python 3.7 meant that attrs became “legacy”, if not downright “deprecated”. That’s despite the fact that attrs is a strict superset of what dataclasses provides. And despite the fact that at the time 3.7 was still “the future”10 and to use it you had to include a backport of dataclasses as a dependency for Python 3.6 and older.
Some individuals felt – and still feel – it necessary to campaign for other projects to drop dependency on attrs, and to mention dataclasses in any conversation that talks about attrs11. At times this led to outright erasure and misrepresentation of what the history of the two libraries is and what the current status of attrs is.
I even suffered some direct hostilities for keeping attrs alive. The starkest contrast was between PyCon US 2017 where people kept thanking me for attrs and PyCon US 2018 where people kept asking me about the point of attrs’s continued existence.
And how do you even fight misinformation – whether on purpose or not? Do I reply to every tweet that gets it wrong? Do I comment under every blog post? Do I have a more of a comment than a question after every conference talk? I might be right, but such behavior would put me in the petty, bitter dinosaur box that I’d like to avoid for a few more years.
To this day I’m baffled by what happened and it sucked every bit of fun and joy that I had with Open Source for a long time. To be crystal clear: I don’t expect everyone to use attrs and/or shower me with admiration; all I ask for is a base level of respect and that people don’t spread lies and misinformation.
Despite all of this, attrs’s usage grew over the years, cleared 1 billion downloads, got supported first by PyCharm, then much later by pyright/pylance/VS Code, and gained me and my contributors a sweet NASA badge on GitHub.
It is indisputable though that
@attr.s(auto_attribs=True) is clunky. And that
@attr.dataclass irrevocably destroys the idea with what the namespace started. And finally, that some of
@attr.s’s defaults are inconvenient, but can’t be changed without breaking people’s code. My own codebases were littered with
@attr.s(auto_attribs=True, slots=True) too and I found it annoying.
So, we decided to look for better names for the decorator and we found it:
@define with idiomatic aliases
@define(frozen=True)). These APIs along with
field() as a replacement for
attr.ib() landed in attrs in version 20.1.0 (August 2020).
This allowed us to re-do all defaults anew and have nicer names without breaking anybody’s code – a rare luxury! People who read announcements and changelogs loved the new APIs:
from attr import define @define class Point: x: int y: int
It’s beautiful and the API “speaks” to you. Reading it out aloud tells you what’s happening.
With these new APIs, you also get a correct collection of attributes according to the MRO12, support for Exceptions, automatic detection of existing methods (i.e. no
init=False necessary if you implement a
__init__), slotted classes, and much, much more by default.
With that out of the way, what remained was the ugly namespace.
The Last Step:
Welcome to today – welcome to attrs 21.3.0! We have arrived at what I call modern attrs:
from attrs import define @define class Point: x: int y: int
attrs import namespace currently simply re-imports (almost) all symbols from the old
attr one that is not going anywhere. Notable exceptions are
attrs.astuple() that also got better defaults.
It shouldn’t have taken me this long to take the last step, although it was a whopper of a pull request. It held back the 21.3.0 release for months and it stopped me from working on new features, because they would’ve complicated the process further.
In the end I fell victim to trying to get the
attrs namespace as perfect as possible, because it was such a unique chance for a clean slate: “Just one more fix, then it will be perfect!”
I hope you like these changes as much as we do. We’re committed to keeping attrs at the brave frontier of class boilerplate reduction and the report of us being a legacy solution was an exaggeration. attrs is not only actively maintained, it’s actively under development.
It has evolved from something like dataclasses to a flexible toolkit for assembling and inspecting classes and it remains the best solution for those who either don’t want to use type hints at all, or need a solution to embrace typing gradually.
If you want a good-faith attempt at comparing attrs to dataclasses, we have updated the relevant section in our documentation – maybe you’ll consider it for your next project, too?
Finally, I need to thank everyone who helped me through the Dark Times and who supported me with encouragement, code, and money. Maintaining a project like attrs for more than half a decade and taking it through paradigm shifts is an exhausting Marathon and especially my GitHub Sponsors and Tidelift Subscribers helped me to motivate myself to keep pushing.
I’m exhausted, but happy. attrs has been in production since 2015 and I hope I’ve put it on track for at least another 7 years – reinventing itself as often as necessary.
You’re more than welcome to join us if that sounds interesting to you!
Which in turn was code that I extracted from another project of mine: service-identity. ↩︎
I still consider the short names very nice, because by separating the identical stem (
attr) from the differentiator (
ib) I find them better discernible than their serious business aliases
attrib. The explicit namespace is also helpful when reading code and you import only one short symbol –
attr– and have the full toolkit in your hands. YMMV. ↩︎
Back in 2015, it looked abandoned with having only one release in 2013 so I didn’t feel bad about using the same namespace. There have been releases in 2017 which makes me feel bad about the whole thing. Sorry, Denis! ↩︎
What was I supposed to do? Call it
Although you could always step through attrs-generated code with a debugger. That was always central to attrs’s design. ↩︎
This example is verbatim from the official Python documentation. ↩︎
That’s a reference, not megalomania. ↩︎
Hilariously, I’ve read the email at Amsterdam airport. ↩︎
If you’re interested why attrs itself didn’t end up in the standard library or how that story went on, I recommend to check out the highly readable PEP. I said I’m neutral on adding attrs myself, but if you read my whole comment, you’ll notice I was more of a
-0– at best. ↩︎
Just as 3.10 is only slowly gaining adoption right now, at the time of writing of this post. ↩︎
Before someone tries to be very intelligent: there’s a difference between “I don’t need this, therefore I won’t use it” and “I don’t need this, therefore I’ll tell everyone to stop using it”. ↩︎
dataclasses get it wrong. ↩︎