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.
Before this lesson: Python Classes and Objects, Python @property: Getters and Setters
2Python Dunder Methods: diagram
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:
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 callsIndexing 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.
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.
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:
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.
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.
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.
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
- Python OOP Cheat Sheet: Classes, Inheritance, Dunder Methods — In Python OOP you define a `class` as a blueprint, build objects with it, store data in attributes set in `__…
- Python str() vs repr(): What Is the Difference? — `str()` returns a **readable** version of an object meant for users, while `repr()` returns an **unambiguous*…
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.
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.
- Line 1-3
Moneystores an amount in whole cents. Integers avoid the rounding surprises you get when adding floats such as 0.1 and 0.2. - Line 5-8
__add__is what+runs. It returns a newMoneyand leaves both operands alone. For anything that is notMoneyit returnsNotImplementedinstead of crashing. - Line 10-13
__eq__compares the cents, so two differentMoneyobjects holding the same amount are equal. - Line 15-16
__lt__defines what "less than" means.sorted()needs nothing more than this method. - 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. - Line 21-22
Money(250) + Money(175)becomesMoney(250).__add__(Money(175)), giving 425 cents.print()falls back to__repr__because there is no__str__. - Line 23
==runs__eq__and<runs__lt__: 425 equals 425, and 425 is less than 500, so both areTrue. - Line 24
sorted()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)}✓ 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)✓ 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))✓ 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)✓ 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 ✓ CompletedBookshelf 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.
class Bookshelf:
def __init__(self, books):
self.books = list(books)
# add __len__ and __getitem__ here
shelf = Bookshelf(["Dune", "Emma", "Ulysses"])
print(shelf.books)__len__(self) returns len(self.books). __getitem__(self, index) returns self.books[index], and that one method is enough for indexing and loops.class Bookshelf:
def __init__(self, books):
self.books = list(books)
def __len__(self):
return len(self.books)
def __getitem__(self, index):
return self.books[index]
shelf = Bookshelf(["Dune", "Emma", "Ulysses"])
print(len(shelf), shelf[0], shelf[-1])
for book in shelf:
print(book)
2. Predict the score arithmetic
Predict the output Medium ✓ CompletedScore knows how to add another Score or an int, but only when the Score is on the left. Type exactly what the program prints.
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")3 + a asks int first, and int returns NotImplemented. Then Python looks for a.__radd__. Does Score define one?Score(15)
Score(13)
True False
3 + a fails
2 more exercises → 6 more examples
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