Dunder methods (short for double underscore) are special methods that Python calls behind the scenes. They let us define how our objects behave with built-in operations like +, len(), print(), and even in.
Think of them like hooks — Python gives us specific spots to plug in custom behavior.
Object Basics
We’ve already seen __init__. Here are the other essentials:
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def __str__(self): # print(book) — friendly output
return f"'{self.title}' ({self.pages} pages)"
def __repr__(self): # repr(book) — developer output
return f"Book('{self.title}', {self.pages})"
def __len__(self): # len(book)
return self.pages
The only difference between __str__ and __repr__: __str__ is for humans, __repr__ is for developers. If only one is defined, Python falls back to __repr__ for everything.
Comparison Methods
These let us use ==, <, >, etc. with our objects.
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def __eq__(self, other): # book1 == book2
return self.pages == other.pages
def __lt__(self, other): # book1 < book2
return self.pages < other.pages
def __gt__(self, other): # book1 > book2
return self.pages > other.pages
Pro tip: if we define __eq__ and __lt__, we can use functools.total_ordering to auto-generate the rest (<=, >=).
Arithmetic Methods
We can make our objects work with +, *, and more.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar): # v1 * 3
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v = Vector(1, 2) + Vector(3, 4)
print(v) # Vector(4, 6)
Container Methods
These make our objects behave like lists or dicts.
class Playlist:
def __init__(self, songs):
self._songs = songs
def __getitem__(self, index): # playlist[0]
return self._songs[index]
def __setitem__(self, index, value): # playlist[0] = "new song"
self._songs[index] = value
def __contains__(self, item): # "song" in playlist
return item in self._songs
def __len__(self): # len(playlist)
return len(self._songs)
Callable Objects
__call__ lets us use an object like a function.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
print(double(5)) # 10 — calling the object like a function
Context Manager Methods
__enter__ and __exit__ let our objects work with the with statement.
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, *args):
import time
print(f"Elapsed: {time.time() - self.start:.2f}s")
with Timer():
sum(range(1_000_000)) # Elapsed: 0.03s
__hash__
If we define __eq__, Python automatically sets __hash__ to None (making the object unhashable). To use our objects in sets or as dict keys, we need to define __hash__ too.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
In simple language, dunder methods are Python’s way of letting us teach our objects how to behave with built-in operations. Almost everything in Python — from + to len() to for loops — is powered by dunder methods under the hood.