Skip to content

Dunder methods

~20 min ยท prerequisite: Polymorphism and protocols

What you will be able to do

  • Make your own class work with operators such as +, *, and == by writing dunder methods.
  • Give an object a readable text form with __repr__ and use it with print, repr, and str.
  • Use len, in, and sorted with your own classes, and know when a class can be used in a set.

The idea

A dunder method is a method whose name starts and ends with two underscores, such as __add__. ("Dunder" is short for "double underscore.") Python calls these methods when you use an operator or built-in function on your object. When you write v1 + v2, Python calls v1.__add__(v2). When you write len(store), Python calls store.__len__().

__add__ handles +, and __mul__ handles *. __eq__ handles ==, and __lt__ handles <. __len__ handles len, and __contains__ handles in. __repr__ gives the official text form of an object, precise enough to rebuild it, such as Vector(1, 2). print and str use __str__ when a class defines it, and otherwise fall back to __repr__.

Defining __eq__ also sets __hash__ to None, so the object can no longer be a set element or dictionary key. To allow that, define __hash__ too, so that equal objects give equal hash values.

The standard library's dataclasses module can write __init__, __repr__, and __eq__ for simple classes. This lesson writes them by hand. See the dataclasses documentation for the shortcut.

Worked example

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"


v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)
print(v1 * 3)
print(v1 == Vector(1, 2))

The output is Vector(4, 6), then Vector(3, 6), then True. Line by line, this is what Python does:

  1. __add__ runs when Python sees + between two vectors, and __mul__ runs for * with a number on the right. Each returns a new Vector.
  2. __eq__ runs for ==, and returns True only when both parts match.
  3. __repr__ builds the text Vector(x, y), which print uses here.
  4. v1 + v2 calls v1.__add__(v2), giving Vector(4, 6). v1 * 3 calls v1.__mul__(3), giving Vector(3, 6).
  5. v1 == Vector(1, 2) calls __eq__, and the parts match, so the result is True.

Try it

The first block tests the operators and text form, the second adds len and in to an inventory, and the third makes objects sortable.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"


v1 = Vector(1, 2)
v2 = Vector(3, 4)
assert v1 + v2 == Vector(4, 6)
assert repr(v1 * 2) == "Vector(2, 4)"
assert v1 != v2
assert str(v1) == "Vector(1, 2)"
class Inventory:
    def __init__(self):
        self.items = []

    def add(self, name):
        self.items.append(name)

    def __len__(self):
        return len(self.items)

    def __contains__(self, name):
        return name in self.items


store = Inventory()
store.add("pen")
store.add("mug")
assert len(store) == 2
assert "pen" in store
assert "book" not in store
class Price:
    def __init__(self, amount):
        self.amount = amount

    def __lt__(self, other):
        return self.amount < other.amount

    def __repr__(self):
        return f"Price({self.amount})"


prices = [Price(5), Price(2), Price(9)]
ordered = sorted(prices)
assert ordered[0].amount == 2
assert ordered[-1].amount == 9

Common mistakes

  1. Adding a number to a vector. Writing Vector(1, 2) + 5 calls __add__ with other set to 5. Inside it, other.x fails:

    AttributeError: 'int' object has no attribute 'x'
    
  2. Misspelling the initializer name. Writing def _init_(self, x, y): with one underscore on each side means Python never calls it. Then Vector(1, 2) gives:

    TypeError: Vector() takes no arguments
    
  3. Forgetting to return a value. If __add__ creates a vector but does not return it, the result of v1 + v2 is None. Then (v1 + v2).x gives:

    AttributeError: 'NoneType' object has no attribute 'x'
    

    Every operator method must return the result.

  4. Putting an object with __eq__ into a set. Defining __eq__ without __hash__ makes the object unhashable. Writing {Vector(1, 2)} gives:

    TypeError: cannot use 'Vector' as a set element (unhashable type: 'Vector')
    

    Define __hash__ as well if the object must go in a set.

  5. Expecting == to compare contents without __eq__. A class with no __eq__ compares identity, so assert Vector(1, 2) == Vector(1, 2) fails with:

    AssertionError
    

Practice

  1. Give a class Price a __repr__ that returns Price(5) for Price(5). Assert that repr(Price(5)) equals "Price(5)".
  2. Create a class Shelf with an items list, an add(self, name) method, and a __len__ method. Add two names and assert that len(shelf) is 2.
  3. Add a __sub__ method to Vector that subtracts part by part. Assert that Vector(5, 5) - Vector(1, 2) equals Vector(4, 3).
  4. Add a __str__ method to Vector that returns "(1, 2)" style text. Assert that str(Vector(1, 2)) is "(1, 2)", and that repr(Vector(1, 2)) is still "Vector(1, 2)".
  5. Stretch: create Money with amount, an __add__ that returns a new Money, an __eq__, and a __hash__ that returns hash(self.amount). Assert that (Money(2) + Money(3)).amount is 5, and that len({Money(1), Money(1)}) is 1.
Solution
  1. ```python class Price: def init(self, amount): self.amount = amount

    def repr(self): return f"Price({self.amount})"

assert repr(Price(5)) == "Price(5)" ```

  1. ```python class Shelf: def init(self): self.items = []

    def add(self, name): self.items.append(name)

    def len(self): return len(self.items)

shelf = Shelf() shelf.add("pen") shelf.add("mug") assert len(shelf) == 2 ```

  1. ```python class Vector: def init(self, x, y): self.x = x self.y = y

    def sub(self, other): return Vector(self.x - other.x, self.y - other.y)

    def eq(self, other): return self.x == other.x and self.y == other.y

    def repr(self): return f"Vector({self.x}, {self.y})"

assert Vector(5, 5) - Vector(1, 2) == Vector(4, 3) ```

  1. ```python class Vector: def init(self, x, y): self.x = x self.y = y

    def str(self): return f"({self.x}, {self.y})"

    def repr(self): return f"Vector({self.x}, {self.y})"

assert str(Vector(1, 2)) == "(1, 2)" assert repr(Vector(1, 2)) == "Vector(1, 2)" ```

  1. ```python class Money: def init(self, amount): self.amount = amount

    def add(self, other): return Money(self.amount + other.amount)

    def eq(self, other): return self.amount == other.amount

    def hash(self): return hash(self.amount)

assert (Money(2) + Money(3)).amount == 5 assert len({Money(1), Money(1)}) == 1 ```

Recap

  • A dunder method such as __add__, __eq__, or __len__ runs when Python sees the matching operator or built-in function on your object.
  • __repr__ gives the text form of an object, and print uses it when no __str__ is defined.
  • Defining __eq__ makes the object unhashable unless you also define __hash__.

Sources