Dunder methods — short for “double underscore” methods — are Python’s mechanism for letting your classes hook into the language’s built-in behavior. When you write len(obj), Python calls obj.__len__(). When you write a + b, Python calls a.__add__(b). Understanding dunders is the key to writing classes that feel native to Python.
What is a Dunder Method?#
A dunder method has a name surrounded by double underscores: __init__, __str__, __add__, etc. Python defines dozens of these hooks. You override them in your classes to customize how instances behave with operators, built-in functions, and language constructs.
Initialization: __init__ and __repr__#
Every class starts here:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v = Vector(3, 4)
print(v)
# Output: Vector(3, 4)
__repr__ returns a string that ideally could recreate the object. It’s what you see in the REPL and in debugging output.
String Representation: __str__ vs __repr__#
__str__ is for human-readable output; __repr__ is for developers:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
v = Vector(3, 4)
print(repr(v)) # Vector(3, 4)
print(str(v)) # (3, 4)
print(v) # (3, 4) — print() calls __str__
If you only define one, define __repr__. Python falls back to it when __str__ isn’t defined.
Operator Overloading: __add__, __sub__, __mul__#
This is where dunders shine — making your classes work with +, -, *, etc.:
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 __sub__(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 __repr__(self):
return f"Vector({self.x}, {self.y})"
a = Vector(1, 2)
b = Vector(3, 4)
print(a + b) # Vector(4, 6)
print(b - a) # Vector(2, 2)
print(a * 3) # Vector(3, 6)
Comparison: __eq__, __lt__, and @total_ordering#
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __lt__(self, other):
return (self.x ** 2 + self.y ** 2) < (other.x ** 2 + other.y ** 2)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
a = Vector(1, 2)
b = Vector(3, 4)
print(a == b) # False
print(a < b) # True (magnitude 2.24 < 5.0)
Define __eq__ and __lt__, then use functools.total_ordering to get <=, >, >= for free.
Container Protocol: __len__, __getitem__, __contains__#
Make your class behave like a list or dict:
class Deck:
def __init__(self):
suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
ranks = ['2', '3', '4', '5', '6', '7', '8', '9',
'10', 'J', 'Q', 'K', 'A']
self.cards = [f"{r} of {s}" for s in suits for r in ranks]
def __len__(self):
return len(self.cards)
def __getitem__(self, index):
return self.cards[index]
def __contains__(self, card):
return card in self.cards
deck = Deck()
print(len(deck)) # 52
print(deck[0]) # 2 of Hearts
print("A of Spades" in deck) # True
# __getitem__ also enables iteration and slicing
for card in deck[:3]:
print(card)
# 2 of Hearts
# 3 of Hearts
# 4 of Hearts
Context Manager: __enter__ and __exit__#
These dunders power the with statement:
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self.start
print(f"Elapsed: {self.elapsed:.4f}s")
return False
with Timer():
total = sum(range(1_000_000))
# Output: Elapsed: 0.0234s
Callable Objects: __call__#
Make instances callable like functions:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
This is useful for creating configurable function-like objects, and it’s how many Python frameworks implement decorators and middleware.
Why Dunders Matter#
Dunder methods are what make Python feel like Python. They let your custom classes participate in the language’s syntax — operators, for loops, with statements, len(), in, subscripting — instead of requiring users to call custom method names. A class with well-implemented dunders is a class that feels native.
See also:
- Python Magic Methods — an overview of magic methods and common operator overloads
- Context Managers in Python —
__enter__and__exit__as a practical dunder use case