Lazy Looping: The Next Iteration by Trey Hunner
Published October 25, 2019
This video features Trey Hunner at DjangoCon US 2018 in San Diego, California, USA.
DjangoCon US 2018 - Easier Classes: Python Classes Without All The Cruft by Trey Hunner
When bundling up data, sometimes tuples and dictionaries don’t quite cut it. Python’s classes are powerful tools for data storage and manipulation, but it can take quite a bit of boilerplate code to make a well-behaved Python class. In this talk we’re going to discuss how a well-behaved class should work and take a look at a number of helper libraries for creating well-behaved classes.
We’ll first see how to make classes with proper string representations, comparability, iterability, and immutability. Then we’ll dive into helper tools built-in to the standard library and available in third-party libraries and briefly discuss which of these tools makes sense to use with Django’s classes.
We’ll look at namedtuple, NamedTuple (not a typo), attrs, and the new Python 3.7 dataclasses.
Most of the libraries discussed in this talk are only available in Python 3, so if you’re not using Python 3, hopefully this talk will encourage you to upgrade.
This talk was presented at: https://2018.djangocon.us/talk/easier-classes-python-classes-without/
LINKS:
Follow Trey Hunner 👇
On Twitter: https://twitter.com/treyhunner
Official homepage: https://treyhunner.com
Follow DjangCon US 👇
https://twitter.com/djangocon
Follow DEFNA 👇
https://twitter.com/defnado
https://www.defna.org/
Friendly Python classes should behave naturally for the developers who use them: they should have useful representations, sensible equality and ordering, and support operations such as iteration, hashing, or arithmetic when those operations make sense. Trey Hunner shows how implementing dunder methods provides these behaviors, but notes that doing so manually often creates boilerplate; `functools.total_ordering`, `namedtuple`, `attrs`, and especially `dataclasses` can generate much of it. He compares these tools and their trade-offs, explaining that `namedtuple` should only be used when tuple behavior is genuinely wanted, while `dataclasses` can provide concise, immutable, comparable classes without unwanted tuple features. He also argues that developers should first ask whether a class is needed at all, since a couple of well-designed functions may be friendlier than a class.
Summarised automatically from the transcript.
Automatically transcribed, so expect mistakes in names and technical terms.
Yeah, I think that's a good thing. Alright, so uh we are going to be time crunched, so we're gonna get started. I'm not really gonna introduce myself because I don't have time. I will say that I teach Python. So I run a Python skill building service called Python Morsels. I have stickers. If you want one, find me afterward. And I teach uh on-site with companies. I actually do Python Django training as well. That is me. We're going to talk about classes. I am very tempted to say that I'd like to talk about Pythonic classes, but that word pythonic is a little bit controversial or loaded. Uh and it's
definitely a little bit overused. So instead, I want to say that we're talking about friendly classes. Our goal is to make classes that are friendlier for other developers to use. So friendly classes have a nice string representation and they can be compared to each other in ways that make sense. Uh more than that, even they can uh overload operators. So you can use plus, minus, any type of operators that make sense on your class. Maybe iterability, hashability, there's a lot of stuff we could implement on our class to make it friendlier for other developers to use. So the first example we're going to look at, we're going to look at two examples here. First one's a month class. We want this class to accept attributes, which means we're going to need an initializer.
We also want this class to have a helpful string representation, which means we have to have a DunderREPR method. Dunder, by the way, stands for double underscore. And these are essentially special methods that allow us to communicate with Python. This is a contract we have with the Python interpreter. If we want our class to also be comparable using, for example, equality and inequality, we're going to need Dunder EQ. uh on our class. Now we don't need dunder any. We did in Python 2. Python 3 you get that for free if you've got a Dunder EQ. If we want to go even further though and not just have comparable classes but classes that are sortable or orderable uh we're gonna need to implement uh less than, which means we need a dunder
LT method. We also need greater than though, that's gender GT, and there's more than just less than and greater than there's less than and equal to. That's dunder L E, and greater than or equal to is Dunder G E. So after all of this, our class is going to be pretty big, and it's going to have a lot of code duplication. So all of these uh comparison methods, all these Dunder methods here, are pretty much the same method. The only real difference between these methods is the operator that they all use. So there is a helper in the Python standard library that will make this class a little bit shorter, make it a little bit easier for us to create this class. It's called Total Ordering. It lives in the func tools module. It allows us to make dunder EQ and Dunder LT and it figures out the other ones from these.
It kind of extrapolates from those to figure out how to make those others for us. So this is much shorter than the class that we just had, but there's still essentially a lot of boilerplate code. In fact, I would argue this entire class is just boilerplate code. We don't really need to implement all this ourselves. We could just copy paste it from somewhere on the internet. So we're gonna take a look at how to make this class a little bit better. Before we do that, I want to look at our second code example. So we made a month class here. I also want to make A point class. So our point 's gonna be a little bit different from month. We want a three-dimensional point that will also have uh an initializer because we want to accept three arguments for our three dimensions there. Uh it will also be comparable. Oh well, so it also has a nice string representation and it's comparable.
So all three of those we had in our month, but we don't want it to be orderable necessarily. So we don't want our point to be orderable. We do, though Want to be able to do this. So this is tuple unpacking or multiple assignment. We want to be able to unpack our point object into X, Y, and Z variables. To do this, we need to make our class iterable. To make our class iterable, we need it under iter method. So we can actually make our own classes work kind of like this, kind of like tuples do in Python If we wanted to, we could also make our class immutable. What immutable means is that if we try to assign to an attribute on our class, say p. x equals 4, we get an error.
So we're setting our class in stone once we've created it. We can't actually change those attributes once we've made our class. For this, this one's a little weird. We need a dunder set adder, which you pretty much never see. And once we've made a dunder set adder, we might decide, sort of like if you give a mouse a cookie, we need our class to also be hashable. So we want our class to work as in dictionary keys, we want it to work in sets. For that, though, we need a Dunder hash method. So if we want a well-behaved point class, something that works as you would expect it to, if we were making a dream class ourselves, this might be what it would look like. Now the code for this isn't actually that long, at least for each method, it's only a couple lines of code. This is still entirely boilerplate code though. So some of this might be new to you, especially if you're new to Python.
For example, that yield there, that's a generator. You can look up generators and iterators, Google that on your own later. Uh Dunder setAder is kind of weird. Hash is kind of weird. A lot of these concepts are a little bit odd. Uh this is basically copy paste code time here. We can take this from the internet, copy-paste it, and we've made a class ourselves that does what we want, which means this is all boilerplate code. This is code that someone else could have created for us. So there is fortunately a way to do this. Again, using a tool in the standard library, it can um that can implement a lot of this for us. It is called NameTuple. And it's built into Python 3. So Nametuple lives in the typing module.
To create a nametuple class, you inherit from name tuple. And then you use type hints to define the attributes that you want on your class. What this does for us is it actually makes a class that has an initializer automatically and it has a friendly string representation, which is what we're looking for. And this class, as a bonus, which is what we're looking for, is uh comparable and in fact it's even immutable, meaning if we try to uh assign to an attribute on our class we'll get an error. By the way, if you're a little bit confused right now, you're going, what is this capital N capital T name to? Well, I've seen the lowercase version. That existed in Python 2. That's in the collections module. It still exists in Python 3. It's a little bit more awkward though.
If you're in Python 3 land, this is what you should use. use. So out-of-the-box name tuple pretty much gives us what we're looking for in our point class at least. But I don't think you should use name tuple to implement this point. This looks cool, but it's not actually what we want. Uh the reason for that are there are some quirks, there are some downsides to name tuple. So name tuples inherit from tuples, meaning they inherit all of the functionality that tuples have within them so they can be ordered. Which is a little bit weird for our point. We don't actually want our points to be orderable. We want them to be comparable. Less than on two points doesn't make so much sense though, maybe. They can also be added to each other, which is actually kind of nifty. It seems like that would be a helpful feature of our points.
But when we add two points together, we don't really get the thing we were looking for. We get a tuple that uh well we get two tuples added together because name tuples are in fact tuples. When you add two tuples together, this is what happens. The same thing happens with multiplication. You can multiply tuples by numbers You just get a tuple back. So name tuples are great, but they're not always the thing you're looking for. They also have a length. Maybe that makes sense for a point. It's sort of weird that it has a length of three, though. Name tuples are wonderful, but they're not always the thing that you should use. When you're using them, you want to be careful. You want to be careful to make sure the thing you're looking for is actually a tuple. Now we could actually get all of these features that we wanted without these features here. We could remove these features
by implementing a lot of methods that just raise exceptions. This is really awkward though. Finding and removing features that you don't want is often harder than adding the features that you do want. And that's the case with name tuple. So don't use name tuple unless the thing you're actually trying to make should act like a tuple. If you're not trying to make a tuple, don't use name tuple. So instead of name tuple, we could use adders. This is an alternative. Uh this unfortunately is a third-party library. It's not built in the Python, which means to get it we have to pip install it, which isn't a big problem. We can pip install adders. After this, we import adder. We don't import adders, we import adder. So we pip install adders, we import adder. To use it, we
uh use attr. s. which is a little bit cute but also a little bit weird. Uh to decorate this class, we set auto attribts equal to true so that we can use type hints, which is just like our name tuple used there. And after this, we got pretty much the same thing that we get from Nameduple. We've got a class that has a constructor, an initializer automatically. Uh it has a nice string representation and it's uh comparable, meaning we can ask to uh adder objects here whether they're equal to each other. Unfortunately, We can also use less than and greater than, which is a little bit weird for point. We want to figure out how to disable this feature here. We don't want them to be orderable So if we want our point object to work uh the same way we had it before
when we made it manually, we could do this with this code here. So we're still using adders here, but we've set frozen to true. So our class can't be modified. It's immutable now. And we've made a dunder iter that we've made manually to make our class actually iterable. kind of like our name tuple was, and we've disabled comparisons. Unfortunately, because we disabled comparisons, we need to make our own custom dunder EQ and we need to make our own custom dunder hash, which at this point is starting to look like maybe we should have just implemented this class on our own. So the Adder 's library, it's more powerful than name tuples, and it's more flexible than name tuples, but it can take a little bit of uh playing with to get it to do exactly what you're looking for for the simple case. So adders, in addition, unfortunately it is a third-party library.
We have to pip install it to use it. There is actually though something that's included with Python that works like adders. It is data classes. So data classes are essentially a simplified version of the Adders library that's built into the Python standard library. To use data classes, you'll import data class from data classes. and you'll decorate your class the same way that you can with adders. The syntax that this relies on here is again still type hinting. So like adders, like name tuple, we've got type hints there. which makes our class also have an initializer, has a nice string representation, and it can be compared using equals. It's Gives us an error though, which is what we're looking for when we use less than.
So it can't be ordered. We'd have to turn that on. We'd have to do that ourselves if we want our point object to be ordered. So this point object, it looks close to what we're looking for, but it isn't quite what we want yet. If we're trying to use our point with multiple assignment, for example. We'll get an error because our point class here is not iterable. So we can't unpack it into x, y, and z variables. If we want to try to assign to the x attribute on our class It works, which is actually a problem in our case because we wanted an error here. We wanted our class to be immutable. So we're expecting to get an error here and we don't. Uh we need to make our point class iterable and immutable. To do that with data classes, uh
we can implement a dunder iter method. So this is not a data class specific thing. We're just doing the same thing we were doing before and making a dunder iter method to make our class iterable, meaning we can use multiple assignment. So this code is the same code we wrote before, that's self. x, self. y, self. z. Uh we could have copy pasted this. We could instead copy paste this. So data classes have an as tuple helper that allows us to get a tuple that contains the objects or rather the attributes that are on our data class. object instead of specifying those manually, which isn't a huge helper, but it is kind of nifty. That means if you copy paste this code, it's going to work in all data classes. So our point class now is uh iterable, meaning that we can use multiple assignment, which is great.
This is just what we're looking for. It isn't yet immutable, meaning we can assign to attributes and we don't get an error still. So to make it immutable, we can set frozen to true. So this is very similar to adders. So when we decorate our class with frozen as true there, uh we now can't assign to attributes in our class. This is exactly what we're looking for. So our class is now iterable, it's immutable, it's comparable, it has a nice string representation. It's not Which is what we're looking for. It doesn't have a length, and it doesn't have all those other weird features that name tuples had that we didn't want. So using data classes, we have made a friendly class. in less than 10 lines of Python code.
I think this is pretty cool. Data classes is doing a lot of work for us here that we could have done ourselves, but it's just doing a little bit of that work that we wouldn't have to do on our own. Now I said that data classes is built into the Python standard library. What I meant by that is that it's built into Python 3. 7. Who is using Python 3. 7 in production? Your production server, okay. Rounding up. Uh uh notice I'm I'm not raising my hand. I'm not using Python 3. 7 in production at the moment. Uh this fortunately is also available as a third-party library, so you can actually pip install data classes to prepare yourself for that day that you upgrade to Python 3. 7. 3. 7.
In Python 3, you can. In Python 2, you cannot pip install data classes. If you need to support Python 2, I would recommend using adders. It supports Python 2 and Python 3, even though it's a little bit more awkward for some cases. And if you do need more features than data classes provide, adders is a little bit more feature-rich in some ways. A little bit more complex to use for the simple cases, more feature-rich in general, though. Uh and data classes, by the way, was partially inspired by adders. That's kind of the timeline that happened here. So uh the thing that I want to talk about next, I don't mean to be an infomercial for data classes. But it's probably an infomercial for data classes. So without any other customization, data classes give us a good string representation.
They allow our objects to be compared to each other. We get an initializer. And that's pretty much what we get out of the box. If we want to turn on other features, we can do so. So for example, we just saw that we can use frozen equals true to make our data classes immutable. That also, by the way, makes our data classes hashable. So if you wanted to use them as keys in dictionaries or in sets, that's a nice side effect. And it makes our data classes um uh iterable. I mean we we can if we wanted to use something besides uh sorry not iterable uh iterable is this one. Uh this would make our data classes iterable here so that we could use it in multiple assignment, but we can also, as you see there with a tuple uh actually make these iterable in any other way. We could use a list constructor, for example. So
something I haven't shown you here, we were talking about that month class earlier. I kind of dropped that uh example. I want to go back to that for a moment because that required orderability. So we can actually set order to true when we're decorating our data class to uh get lexical graphical ordering, which sounds like a really fancy word, but all it really means is the way that tuples, lists, and all the other orderable structures in Python by default are ordered. So they go the first thing first and then everything after that. So if we want orderability on our class, that's something we can turn on with data classes as well. One more thing I'd like to discuss that is not at all data class specific, kind of like the fact that DunderIter is not actually data class specific. is
Dunder methods. Dunder methods in general. Almost everything we've been doing in this talk is actually just Dunder methods, regardless of whether I've been writing Dunder methods. So One of the secrets, really the main secret I think to making a friendly class in Python is embracing Dunder methods. This is a class here that implements uh DunderIter, DunderAdd, and Dunder Sub. DunderAdd and DunderSub, what they give us are uh addition and subtraction. The thing that that dunderIder method gives us is, well, really nothing in our case outside of the class. Maybe, depending on how we use it, but it makes that dunder add and the dunder sub the implementation a little bit easier. So sometimes you will hear people call dunder methods magic methods. The reason I don't usually use this term is because magic methods are not magical.
They're not magical and they're not scary. So I want you to feel comfortable embracing Dunder methods in Python. Dunder methods can make friendlier classes. Alright, so before we wrap up, I think we're actually doing okay on time, which is surprising to me. Uh I would like you to consider Whether you even need classes. So this is a class that represents a matrix. This class accepts a string that represents a matrix. So it's got new line characters and spaces, and that represents a a matrix here, rows and columns in a matrix. When we construct this thing, the object we get at, that matrix object, has a rows attribute that gives us the rows in our matrix and a columns attribute that gives us the columns in our matrix, the transpose
of this matrix. This class uh does not need to exist. Uh I say that because you could take this class and replace it with two functions. you could make a matrix from string function, and you could make a transpose function. So that matrix from string function, when we call it, we call it just the same way we constructed our matrix object before. for. The thing it gives us back though is the same thing as our rows attribute gave us back on that matrix object. The transpose function transposes our matrix, which gives us the columns back. So we've just taken what was a class and turned it into two functions. You do not always need classes. Friendly classes are great, but sometimes the friendliest classes just don't exist at all.
Friendly functions are also a nice thing in Python. So when making classes, I'd like you to first consider whether you even need a class at all, because often you don't. Uh there is a whole talk on this subject called Stop Writing Classes. It's a bit of an old talk. It is uh Python 2, but it's entirely sp uh applicable rather to Python 3. If you do decide after watching that talk that you need a class, uh I'd like you to make a friendly class. So friendly classes should feel as natural to use as the built-in objects within Python. Lists, tuples, all the things that are built into Python, your class should feel as friendly and as natural as the those. Creating friendly classes can involve a lot of boilerplate code because really it involves writing a lot of dunder methods oftentimes.
Data classes essentially help us write those dunder methods ourselves. They give us those things for free without us having to go out of the way to write them. them. Unfortunately data classes are only in Python 3. 7. You can pip install them. If you're stuck on Python 2 right now, you can use adders. instead. Or if you need more features than data classes actually provide, you can use adders. I'm actually doing really great on time, so we might even have time for questions here. I would love to help you improve your Python skills. That's the thing that I do for a living. Feel free to find me in the hallway afterward if you'd like to chat about improving your Python skills or your team's Python skills or always feel free to send me an email.
If folks have questions, I'd actually prefer to do them the hallway afterward, just so we can kind of huddle around and chat about uh everything we've just seen here. By the way, the slides that I have, there's a whole bunch of extra things in the slides. You can look into those on your own. I'll tweet them afterward. Thank you.
A friendly class has a helpful string representation, supports sensible comparisons and operators, and can provide useful behaviors such as iteration or hashing. These behaviors are usually implemented with Python’s special, or dunder, methods.
Discussed at 0:51Only if the object is genuinely meant to behave like a tuple. Named tuples inherit tuple behavior such as ordering, concatenation, repetition, and having a length, which can be surprising or incorrect for objects like three-dimensional points.
Discussed at 7:01A dataclass can generate an initializer, a useful string representation, and equality comparison from type-annotated attributes. It is a standard-library alternative to writing much of that boilerplate by hand, while avoiding the unwanted tuple behavior of namedtuple.
Discussed at 10:53Add a dunder `__iter__` method so instances can be unpacked, and set `frozen=True` on the dataclass decorator to prevent attribute assignment. A frozen dataclass is also hashable, so it can be used in sets or as a dictionary key.
Discussed at 12:26Set `order=True` on the dataclass decorator. Python then provides tuple-like lexicographical ordering, comparing the first field first and later fields as needed.
Discussed at 16:17Not every abstraction needs a class. The example matrix class could be replaced by a function that parses a matrix string and another that transposes it, so functions are often the friendlier choice when there is no need for persistent object state or class behavior.
Discussed at 18:44Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 14, 2026