Python Dunder Methods
Object-Oriented Python Object-Oriented Python

Python Dunder (Magic) Methods

1Python Dunder Methods: overview

Dunder methods are specially named methods such as len and add that Python calls behind the scenes whenever you use built-in syntax like len(x), a + b or x[0], so your own objects can behave like built-in ones.

GoalBy the end you can add len, eq, lt, add, getitem, contains, bool and call to a class, explain that a + b really runs a.add(b), and return NotImplemented for types you cannot handle.

Before this lesson: Python Classes and Objects, Python @property: Getters and Setters

2Python Dunder Methods: diagram

Python Dunder (Magic) Methodsdunder hookswhat you writewhat Python callslen(box)box.__len__()box == otherbox.__eq__(other)box < otherbox.__lt__(other)box + otherbox.__add__(other)box[0]box.__getitem__(0)item in boxbox.__contains__(item)if box:box.__bool__()Familiar syntax is a shortcut for a method call on your object
Each everyday operation on the left is translated by Python into a call to a dunder method on the right. Define that method in your class and the syntax starts working for your own objects.

3Python Dunder Methods: explained step by step

You have already written __init__, and in the classes lesson you met __str__ and __repr__. Those are dunder methods (short for double underscore, also called magic or special methods). You never call them yourself; Python calls them for you at particular moments. The surprise is how many everyday operations work this way.

Python's syntax is a set of hooks

Every familiar operation is quietly a method call. len(x) runs x.__len__(), a + b runs a.__add__(b), and x[0] runs x.__getitem__(0). Built-in types already define these methods, which is why len("hi") and [1] + [2] work. When you define the same methods in your own class, your objects start behaving like built-in ones.

You will almost never type a dunder name in normal code. You write the method once inside the class, then use the normal syntax and let Python call it.

len() with __len__

Return a whole number and len(obj) works:

Python
class Playlist:
    def __init__(self, songs):
        self.songs = list(songs)

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

mix = Playlist(["Havana", "Levitating", "Sunflower"])
print(len(mix))
print(mix.__len__())     # what Python really calls

Indexing and loops with __getitem__

mix[0] calls mix.__getitem__(0). There is a bonus: if a class has __getitem__ but no __iter__, a for loop still works. Python simply asks for item 0, then 1, then 2, and so on, until the method raises IndexError. Delegating to the list inside means that negative indexes and the end of the loop come for free.

Python
class Playlist:
    def __init__(self, songs):
        self.songs = list(songs)

    def __getitem__(self, index):
        return self.songs[index]

mix = Playlist(["Havana", "Levitating", "Sunflower"])
print(mix[0], mix[-1])
for song in mix:
    print("Now playing:", song)

Equality with __eq__

By default == on your own objects only asks "is this the very same object?". Two separate Point(1, 2) objects are not equal. Define __eq__ to compare what the objects contain.

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

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

print(Point(1, 2) == Point(1, 2))
print(Point(1, 2) == Point(3, 4))
print(Point(1, 2) == "hello")
try:
    print({Point(1, 2)})
except TypeError as err:
    print("TypeError:", err)

The last lines show a side effect worth knowing: once a class defines __eq__ without __hash__, Python marks its objects as unhashable, so they cannot go into a set or be dictionary keys. The reason is a safety rule: objects that are equal must have equal hashes, and Python can no longer promise that. If you need hashing, add a __hash__ that uses the same fields you compare, for example hash((self.x, self.y)).

Ordering with __lt__

sorted() and min()/max() compare objects with <. Define __lt__ ("less than") and your objects can be sorted:

Python
class Runner:
    def __init__(self, name, seconds):
        self.name = name
        self.seconds = seconds

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

    def __repr__(self):
        return f"{self.name}({self.seconds}s)"

field = [Runner("Asha", 312), Runner("Ben", 287), Runner("Chen", 301)]
print(sorted(field))
print(min(field))

Python can flip a > b into b < a on its own, but it cannot turn < into <=. For all six comparison operators from one __lt__ plus __eq__, put @functools.total_ordering above the class (see the examples page).

Arithmetic with __add__, __sub__, __mul__

When Python sees a + b it really runs a.__add__(b). Return a new object, and leave a and b unchanged, just as 1 + 2 does not change the 1.

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

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, factor):
        if not isinstance(factor, (int, float)):
            return NotImplemented
        return Vector(self.x * factor, self.y * factor)

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

a = Vector(1, 2)
b = Vector(4, 6)
print(a + b)
print(b - a)
print(a * 3)
print(a.__add__(b))          # exactly what a + b does
try:
    a + "oops"
except TypeError as err:
    print("TypeError:", err)

Returning NotImplemented

When your method receives a type it does not understand, return NotImplemented (do not raise it). It is a special value meaning "I can't do this with that type, ask the other side". Python then tries the other object's reflected method (__radd__ for +), and only when nobody knows what to do does it raise the clear TypeError: unsupported operand type(s)... you saw above. If you skip the check, a wrong type usually crashes with a confusing AttributeError from deep inside your method instead.

Membership with __contains__

item in obj calls obj.__contains__(item). Without it Python falls back to looping over the object, so define it when you can answer faster or want a custom rule, such as ignoring upper and lower case.

Truthiness with __bool__

if obj: and bool(obj) call __bool__. If a class has no __bool__, Python tries __len__ (zero means false). If it has neither, the object is always true.

Python
class Inbox:
    def __init__(self, messages):
        self.messages = messages

    def __contains__(self, word):
        return any(word.lower() in text.lower() for text in self.messages)

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

class Wallet:
    def __init__(self, cash):
        self.cash = cash

    def __bool__(self):
        return self.cash > 0

inbox = Inbox(["Meeting at 10", "Invoice attached"])
print("invoice" in inbox, "lunch" in inbox)
print(bool(inbox), bool(Inbox([])))
print(bool(Wallet(0)), bool(Wallet(5)))

Objects that behave like functions: __call__

Define __call__ and you can put parentheses after an object: obj(arguments) runs obj.__call__(arguments). It is handy when a "function" also needs to remember settings or a running total.

Python
class Discount:
    def __init__(self, percent):
        self.percent = percent

    def __call__(self, price):
        return round(price * (1 - self.percent / 100), 2)

summer = Discount(20)
print(summer(50))
print([summer(p) for p in [10, 25.5]])
print(callable(summer))

Use them with restraint

Implement an operator only when its meaning is obvious. Vector + Vector is natural; User + User is not, and a plain method such as merge() is clearer. Dunder methods make your classes feel like part of the language, but only when they behave the way a reader would guess.

Quick reference: Python Dunder Methods

4Python Dunder Methods: syntax

class Box:
    def __len__(self):             # len(box)            -> must return an int
        ...
    def __eq__(self, other):       # box == other        -> True / False
        ...                        #   (add __hash__ too if objects go in sets/dicts)
    def __lt__(self, other):       # box < other         -> used by sorted(), min(), max()
        ...
    def __add__(self, other):      # box + other         -> return a NEW object
        if not isinstance(other, Box):
            return NotImplemented  #   "I don't know that type, ask the other side"
        ...
    def __sub__(self, other): ...  # box - other
    def __mul__(self, other): ...  # box * other
    def __getitem__(self, i):      # box[i]  and  for item in box
        ...
    def __contains__(self, item):  # item in box
        ...
    def __bool__(self):            # if box:   (falls back to __len__)
        ...
    def __call__(self, *args):     # box(...)
        ...

5Python Dunder Methods: run the example

Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.

Interactive example
class Money:
    def __init__(self, cents):
        self.cents = cents

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return Money(self.cents + other.cents)

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.cents == other.cents

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

    def __repr__(self):
        return f"Money({self.cents / 100:.2f})"

wallet = Money(250) + Money(175)
print(wallet)
print(wallet == Money(425), wallet < Money(500))
print(sorted([Money(900), Money(50), wallet]))
Expected outputMoney(4.25)
True True
[Money(0.50), Money(4.25), Money(9.00)]

7Python Dunder Methods: how the example works

Hover or tap a step to highlight its lines in the example above.

  1. Line 1-3Money stores an amount in whole cents. Integers avoid the rounding surprises you get when adding floats such as 0.1 and 0.2.
  2. Line 5-8__add__ is what + runs. It returns a new Money and leaves both operands alone. For anything that is not Money it returns NotImplemented instead of crashing.
  3. Line 10-13__eq__ compares the cents, so two different Money objects holding the same amount are equal.
  4. Line 15-16__lt__ defines what "less than" means. sorted() needs nothing more than this method.
  5. Line 18-19__repr__ gives each object a readable form. Lists show their items with __repr__, so this is what appears inside the printed list.
  6. Line 21-22Money(250) + Money(175) becomes Money(250).__add__(Money(175)), giving 425 cents. print() falls back to __repr__ because there is no __str__.
  7. Line 23== runs __eq__ and < runs __lt__: 425 equals 425, and 425 is less than 500, so both are True.
  8. Line 24sorted() compares the objects with < over and over, so the three amounts come out in order, each shown through __repr__.

8Python Dunder Methods: common mistakes

Defining __eq__ and then using the object in a set or as a dict key

✗ Wrong

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

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

visited = {Point(1, 2)}
TypeError: unhashable type: 'Point'

✓ Right

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

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

    def __hash__(self):
        return hash((self.x, self.y))

visited = {Point(1, 2), Point(1, 2)}
print(len(visited))

When a class defines __eq__ without __hash__, Python removes the default hash because equal objects must have equal hashes. Add a __hash__ built from the same fields that __eq__ compares.

Assuming `other` is always the same type

✗ Wrong

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)

print(Vector(1, 2) + 5)
AttributeError: 'int' object has no attribute 'x'

✓ Right

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

    def __add__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

try:
    print(Vector(1, 2) + 5)
except TypeError as err:
    print("TypeError:", err)

Check the type first and return NotImplemented for anything you cannot handle. Python then reports a clear TypeError that names both types, instead of an AttributeError from the inside of your method.

Defining only __lt__ and then using <=

✗ Wrong

class Version:
    def __init__(self, number):
        self.number = number

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

print(Version(1) <= Version(2))
TypeError: '<=' not supported between instances of 'Version' and 'Version'

✓ Right

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, number):
        self.number = number

    def __eq__(self, other):
        return self.number == other.number

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

print(Version(1) <= Version(2))

Python does not derive <= from <. Either write __le__ yourself or let functools.total_ordering generate the missing comparisons from __eq__ and one ordering method.

Expecting 3 * vector to work with only __mul__

✗ Wrong

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

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

v = Vector(2, 5)
print(3 * v)
TypeError: unsupported operand type(s) for *: 'int' and 'Vector'

✓ Right

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

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

    __rmul__ = __mul__

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

v = Vector(2, 5)
print(3 * v)

3 * v asks the int first, and int does not know Vector. Python then tries the reflected method v.__rmul__(3). Define __rmul__ (it can simply do the same as __mul__) so that the number may come first.

9Python Dunder Methods: exercises

Write your answer, then press Check. The checks run in your browser.

1. Make a Bookshelf behave like a sequence

Write code Easy

Bookshelf keeps its books in the list self.books. Add __len__ so len(shelf) returns the number of books, and __getitem__ so shelf[0], shelf[-1] and a for loop over the shelf work.

Your code
class Bookshelf:
    def __init__(self, books):
        self.books = list(books)

    # add __len__ and __getitem__ here


shelf = Bookshelf(["Dune", "Emma", "Ulysses"])
print(shelf.books)

2. Predict the score arithmetic

Predict the output Medium

Score knows how to add another Score or an int, but only when the Score is on the left. Type exactly what the program prints.

Read this code
class Score:
    def __init__(self, points):
        self.points = points

    def __add__(self, other):
        if isinstance(other, Score):
            return Score(self.points + other.points)
        if isinstance(other, int):
            return Score(self.points + other)
        return NotImplemented

    def __eq__(self, other):
        return isinstance(other, Score) and self.points == other.points

    def __repr__(self):
        return f"Score({self.points})"

a = Score(10)
b = Score(5)
print(a + b)
print(a + 3)
print(a == Score(10), a == 10)
try:
    print(3 + a)
except TypeError:
    print("3 + a fails")

2 more exercises → 6 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

Questions learners ask

What are dunder methods in Python?

Dunder (double underscore) methods are special methods like init, len, add and getitem that Python calls for you when you use built-in syntax. len(x) calls x.len(), a + b calls a.add(b), and x[0] calls x.getitem(0). Defining them lets your own classes work with that syntax.

Should I call dunder methods directly?

Normally no. Write len(obj), a + b or obj[0] and let Python call the dunder method. Calling obj.len() yourself works but is harder to read and skips some of Python's fallback rules. The main place you write name calls by hand is inside another dunder method, for example super().init().

Why does defining __eq__ make my class unhashable?

Python requires that objects which compare equal also have the same hash. Once you replace the default eq, Python can no longer trust the default hash, so it sets hash to None. Add your own hash, built from the same fields eq compares, such as hash((self.x, self.y)), if the objects need to go in sets or be dict keys.

Further reading

10Next lesson

Up nextPython InheritanceInheritance lets a new class (the child) start with all the attributes and methods of an existing class (the parent) an…

Related:
Python ListsPython Classes and ObjectsPython Class Methods and Static Methods

← Python @property: Getters and Setters · All Object-Oriented Python lessons · Practise in the IDE