Python in Detail: Dunder Methods
Welcome to the Dunder Methods lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
This course explores deeper details in Python, especially customizing how our Python objects work. To get the most out of this course, we recommend doing some basic projects in Python first.
In the Python for Programmers course, we saw that
lenworks on different types, like lists, dicts, and strings. But that's not becauselenhas special knowledge of lists, dicts, or strings. Instead, when we calllen(some_value), Python actually callssome_value.__len__().>
len(["a", "b", "c"])Result:
3
>
["a", "b", "c", "d"].__len__()Result:
4
That might seem unnecessarily complex at first, but it has an important purpose. By defining our own
.__len__methods, we can customize howlenworks with our objects.>
class Always7Long:def __len__(self):return 7len(Always7Long())Result:
7
Imagine that we want to create a user database object that tracks users in a list.
>
class UserDB:def __init__(self):self.users = ["Amir", "Betty"]def __len__(self):return len(self.users)- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
user_db = UserDB()len(user_db)Result:
2
You might wonder: why bother with this, when we could call
len(user_db.users)? One answer is thatlen(user_db.users)is more tightly coupled, which is usually undesirable. Coupling is when one part of the system depends on another part.The
len(user_db.users)function call couples to the user database in two ways. First, the database must have the.usersattribute that we're accessing. Second, that.usersattribute must have a length. Ourlen(user_db.users)call can only work if both of those facts are true.By calling
len(user_db)directly, we only couple to one fact about the database: the database itself has a length. Now we're free to change the database's implementation, because code outside of the database no longer depends on the.usersattribute.For example, we might replace the
.userslist with an on-disk database that doesn't support.__len__directly. If our system callslen(user_db.users)in dozens of places, all of those calls will now fail, so we'll have to change all of those lines of code. But if our system callslen(user_db)instead, then we only have to update theUserDB.__len__method, and the rest of the system will continue to work.The return value of
.__len__has to be an int. If we return a different data type, that's an error. (You can typeerrorwhen a code example will cause an error.)>
class Always7Long:def __len__(self):return "seven"len(Always7Long())Result:
TypeError: 'str' object cannot be interpreted as an integer
Remember, ints and floats are different data types!
>
class Always7Long:def __len__(self):return 7.0len(Always7Long())Result:
TypeError: 'float' object cannot be interpreted as an integer
Why is the customizable length method named
.__len__? Why not simply name it.len, or.get_length?In many programming languages, prefixing identifiers (like variables and methods) with
_means that something unusual is happening. Python wraps methods like__len__in double underscores to emphasize that we're customizing Python itself. The underscores say "pay extra attention!"In Python, methods like
__len__are called "dunder methods", for "Double UNDERscore". In this course, we'll use dunder methods to customize equality, mathematical operators, iteration, and many more language features. By the end of the course, we'll even be able to customize parts of Python that seem fundamental, like what happens when we accesssome_obj.some_attr.Although you might not yet know what
.__lt__or.__repr__or.__getattr__do, you can now tell that these are dunder methods. Each one customizes some aspect of the class's behavior.Here's a code problem:
The
UserDBclass keeps track of user objects in a list. Each user has a name and a boolean specifying whether the account is active. Define a custom.__len__dunder method that returns the number of active users.Note that the class already has a method that returns a list of active users.
users = [{"name": "Amir","active": True}, {"name": "Betty","active": True}, {"name": "Cindy","active": False}]class UserDB:def __init__(self, users):self.users = usersdef add_user(self, user):self.users.append(user)def active_users(self):return [user for user in self.users if user['active']]def __len__(self):return len(self.active_users())user_db = UserDB(users)assert len(user_db) == 2user_db.add_user({"name": "Dalili","active": True})assert len(user_db) == 3- Goal:
None
- Yours:
None
Dunder methods power a huge amount of Python, so this course will spend a lot of time covering them. We'll mention 106 separate dunder methods, though we'll only discuss about a third of those in detail.
There are some things that we won't cover. This course doesn't cover concurrency or asynchronous programming. It also doesn't cover Python's static type checking features. Both of those topics are large enough to require a full course of their own, or even multiple courses.