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.
Early history
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.s
and 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 @attr.s/attr.ib
:
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. #foreshadow
Things happen
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 namedtuples. This example is verbatim from the official Python documentation:
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 it6.
PEP 557
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 20177. 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.78.
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 social media. 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 practical utility that drove demand. 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 fan-favorite @attr.dataclass
easter egg that was never documented, but was found immediately. 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”9 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 attrs10. 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.
New APIs
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 @mutable
and @frozen
(= @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 MRO11, 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: import attrs
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
The new attrs
import namespace currently simply re-imports (almost) all symbols from the old attr
one that is not going anywhere. Notable exceptions are attrs.asdict()
and 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!”
Well.
Conclusion
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 (s
/ib
) I find them better discernible than their serious business aliases@attrs
andattrib
. 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
attr2
? ↩︎Although you could always step through attrs-generated code with a debugger. That was always central to attrs’s design. ↩︎
Yes, I’m a fan of Cal Newport. ↩︎
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. ↩︎