Execute Program

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 len works on different types, like lists, dicts, and strings. But that's not because len has special knowledge of lists, dicts, or strings. Instead, when we call len(some_value), Python actually calls some_value.__len__().

  • >
    len(["a", "b", "c"])
    Result:
    3Pass Icon
  • >
    ["a", "b", "c", "d"].__len__()
    Result:
    4Pass Icon
  • That might seem unnecessarily complex at first, but it has an important purpose. By defining our own .__len__ methods, we can customize how len works with our objects.

  • >
    class Always7Long:
    def __len__(self):
    return 7

    len(Always7Long())
    Result:
    7Pass Icon
  • 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:
    2Pass Icon
  • You might wonder: why bother with this, when we could call len(user_db.users)? One answer is that len(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 .users attribute that we're accessing. Second, that .users attribute must have a length. Our len(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 .users attribute.

  • For example, we might replace the .users list with an on-disk database that doesn't support .__len__ directly. If our system calls len(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 calls len(user_db) instead, then we only have to update the UserDB.__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 type error when 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 integerPass Icon
  • Remember, ints and floats are different data types!

  • >
    class Always7Long:
    def __len__(self):
    return 7.0

    len(Always7Long())
    Result:
    TypeError: 'float' object cannot be interpreted as an integerPass Icon
  • 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 access some_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 UserDB class 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 = users

    def 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) == 2

    user_db.add_user({
    "name": "Dalili",
    "active": True
    })

    assert len(user_db) == 3
    Goal:
    None
    Yours:
    NonePass Icon
  • 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.