Do you write programs in Python? You should be using attrs.
Why, you ask? Don’t ask. Just use it.
Okay, fine. Let me back up.
But Python is not without its problems. In some cases it encourages you to do the wrong thing. Particularly, there is a deeply unfortunate proliferation of class inheritance and the God-object anti-pattern in many libraries.
One cause for this might be that Python is a highly accessible language, so less experienced programmers make mistakes that they then have to live with forever.
But I think that perhaps a more significant reason is the fact that Python sometimes punishes you for trying to do the right thing.
The “right thing” in the context of object design is to make lots of small, self-contained classes that do one thing and do it well. For example, if you notice your object is starting to accrue a lot of private methods, perhaps you should be making those “public”1 methods of a private attribute. But if it’s tedious to do that, you probably won’t bother.
Another place you probably should be defining an object is when you have a bag
of related data that needs its relationships, invariants, and behavior
explained. Python makes it soooo easy to just define a tuple or a list. The
first couple of times you type
host, port = ... instead of
address = ... it
doesn’t seem like a big deal, but then soon enough you’re typing
[(family, socktype, proto, canonname, sockaddr)] = ... everywhere and your
life is filled with regret. That is, if you’re lucky. If you’re not lucky,
you’re just maintaining code that does something like
values[HOSTNAME][“canonical”] and your life is filled with
garden-variety pain rather than the more complex and nuanced emotion of
This raises the question: is it tedious to make a class in Python? Let’s look at a simple data structure: a 3-dimensional cartesian coordinate. It starts off simply enough:
So far so good. We’ve got a 3 dimensional point. What next?
Well, that’s a bit unfortunate. I just want a holder for a little bit of data, and I’ve already had to override a special method from the Python runtime with an internal naming convention? Not too bad, I suppose; all programming is weird symbols after a fashion.
At least I see my attribute names in there, that makes sense.
1 2 3
I already said I wanted an
x, but now I have to assign it as an attribute...
1 2 3
x? Uh, obviously ...
1 2 3 4 5
... and now I have to do that once for every attribute, so this actually scales poorly? I have to type every attribute name 3 times?!?
Oh well. At least I’m done now.
1 2 3 4 5 6
Wait what do you mean I’m not done.
1 2 3 4 5 6 7 8
Oh come on. So I have to type every attribute name 5 times, if I want to be able to see what the heck this thing is when I’m debugging, which a tuple would have given me for free?!?!?
1 2 3 4 5 6 7 8 9 10 11 12
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Okay, whew - 2 more lines of code isn’t great, but now at least we don’t have to define all the other comparison methods. But now we’re done, right?
You know what? I’m done. 20 lines of code so far and we don’t even have a class that does anything; the hard part of this problem was supposed to be the quaternion solver, not “make a data structure which can be printed and compared”. I’m all in on piles of undocumented garbage tuples, lists, and dictionaries it is; defining proper data structures well is way too hard in Python.2
namedtuple to the (not really) rescue
The standard library’s answer to this conundrum is
While a valiant first draft (it bears many similarities to
somewhat embarrassing and antiquated entry in this genre)
unfortunately unsalvageable. It exports a huge amount of undesirable public
functionality which would be a huge compatibility nightmare to maintain, and it
doesn’t address half the problems that one runs into. A full enumeration of
its shortcomings would be tedious, but a few of the highlights:
- Its fields are accessable as numbered indexes whether you want them to be or
not. Among other things, this means you can’t have private attributes,
because they’re exposed via the apparently public
- It compares equal to a raw
tupleof the same values, so it’s easy to get into bizarre type confusion, especially if you’re trying to use it to migrate away from using
- It’s a tuple, so it’s always immutable. Sort of.
As to that last point, either you can use it like this:
in which case it doesn’t look like a type in your code; simple syntax-analysis tools without special cases won’t recognize it as one. You can’t give it any other behaviors this way, since there’s nowhere to put a method. Not to mention the fact that you had to type the class’s name twice.
Alternately you can use inheritance and do this:
This gives you a place you can put methods, and a docstring, and generally have
it look like a class, which it is... but in return you now have a weird
internal name (which, by the way, is what shows up in the
repr, not the
class’s actual name). However, you’ve also silently made the attributes not
listed here mutable, a strange side-effect of adding the
that is, unless you add
__slots__ = 'x y z'.split() to the class body, and
then we’re just back to typing every attribute name twice.
And this doesn’t even mention the fact that science has proven that you shouldn’t use inheritance.
namedtuple can be an improvement if it’s all you’ve got, but only in some
cases, and it has its own weird baggage.
So here’s where my favorite mandatory Python library comes in.
Let’s re-examine the problem above. How do I make
Since this isn’t built into the language, we do have to have 2 lines of boilerplate to get us started: the import and the decorator saying we’re about to use it.
1 2 3
Look, no inheritance! By using a class decorator,
Point3D remains a Plain
Old Python Class (albeit with some helpful double-underscore methods tacked on,
as we’ll see momentarily).
1 2 3 4
It has an attribute called
1 2 3 4 5 6
And one called
y and one called
z and we’re done.
We’re done? Wait. What about a nice string representation?
1 2 3 4 5 6
Okay sure but what if I want to extract the data defined in explicit attributes in a format appropriate for JSON serialization?
Maybe that last one was a little on the nose. But nevertheless, it’s one of
many things that becomes easier because
attrs lets you declare the fields on
your class, along with lots of potentially interesting metadata about them,
and then get that metadata back out.
1 2 3 4 5
I am not going to dive into every interesting feature of
attrs here; you can
read the documentation for that. Plus, it’s well-maintained, so new goodies
show up every so often and I might miss something important. But
a few key things that, once you have them, you realize that Python was sorely
- It lets you define types concisely, as opposed to the normally quite
def __init__.... Types without typing.
- It lets you say what you mean directly with a declaration rather than
expressing it in a roundabout imperative recipe. Instead of “I have a type,
it’s called MyType, it has a constructor, in the constructor I assign the
property ‘A’ to the parameter ‘A’ (and so on)”, you say “I have a type, it’s
called MyType, it has an attribute called
a”, and behavior is derived from that fact, rather than having to later guess about the fact by reverse engineering it from behavior (for example, running
diron an instance, or looking at
- It provides useful default behavior, as opposed to Python’s sometimes-useful but often-backwards defaults.
- It adds a place for you to put a more rigorous implementation later, while starting out simple.
Let’s explore that last point.
While I’m not going to talk about every feature, I’d be remiss if I didn’t
mention a few of them. As you can see from those mile-long
Attribute above, there are a number of interesting ones.
For example: you can validate attributes when they are passed into an
@attr.s-ified class. Our Point3D, for example, should probably contain
numbers. For simplicity’s sake, we could say that that means instances of
float, like so:
1 2 3 4 5 6 7
The fact that we were using
attrs means we have a place to put this extra
validation: we can just add type information to each attribute as we need it.
Some of these facilities let us avoid other common mistakes. For example, this
is a popular “spot the bug” Python interview question:
1 2 3 4 5 6 7
Fixing it, of course, becomes this:
1 2 3 4 5
adding two extra lines of code.
contents inadvertently becomes a global varible here, making all
objects not provided with a different list share the same list. With
this instead becomes:
1 2 3 4 5 6 7
There are several other features that
attrs provides you with opportunities
to make your classes both more convenient and more correct. Another great
example? If you want to be strict about extraneous attributes on your objects
(or more memory-efficient on CPython), you can just pass
slots=True at the
class level - e.g.
@attr.s(slots=True) - to automatically turn your existing
attrs declarations a matching
All of these handy features allow you to make better and more powerful use of
The Python Of The Future
Some people are excited about eventually being able to program in Python 3
everywhere. What I’m looking forward to is being able to program in
attrs everywhere. It exerts a subtle, but positive, design
influence in all the codebases I’ve seen it used in.
Give it a try: you may find yourself surprised at places where you’ll now use a
tidily explained class, where previously you might have used a
sparsely-documented tuple, list, or a dict, and endure the occasional confusion
from co-maintainers. Now that it’s so easy to have structured types that
clearly point in the direction of their purpose (in their
__repr__, in their
__doc__, or even just in the names of their attributes), you might find
you’ll use a lot more of them. Your code will be better for it; I know mine
Scare quotes here because the attributes aren’t meaningfully exposed to the caller, they’re just named publicly. This pattern, getting rid of private methods entirely and having only private attributes, probably deserves its own post... ↩
And we hadn’t even gotten to the really exciting stuff yet: type validation on construction, default mutable values... ↩