Skip to content

Advanced Dunder Methods

Make your classes work seamlessly with Python's built-in operations.

Dunder methods (double underscore methods) allow you to customize how your objects interact with Python’s built-in operations. While we covered basics in the introduction, here we explore advanced usage patterns.

Make your objects work with Python operators:

operator_overloading.py
class Vector:
"""2D vector with operator overloading"""
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __add__(self, other):
"""Vector addition: v1 + v2"""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other):
"""Vector subtraction: v1 - v2"""
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, scalar):
"""Scalar multiplication: v * 5"""
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
def __rmul__(self, scalar):
"""Right multiplication: 5 * v"""
return self.__mul__(scalar)
def __eq__(self, other):
"""Equality: v1 == v2"""
if isinstance(other, Vector):
return self.x == other.x and self.y == other.y
return False
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 2) # Vector(6, 8)
print(2 * v1) # Vector(6, 8) - uses __rmul__
print(v1 == v2) # False

Use __call__ to make instances callable like functions:

callable_objects.py
class Multiplier:
"""Callable object that multiplies by a factor"""
def __init__(self, factor: float):
self.factor = factor
def __call__(self, value: float) -> float:
"""Make object callable"""
return value * self.factor
double = Multiplier(2.0)
triple = Multiplier(3.0)
print(double(5)) # 10.0
print(triple(5)) # 15.0
# Can be used in higher-order functions
numbers = [1, 2, 3, 4, 5]
doubled = list(map(double, numbers))
print(doubled) # [2.0, 4.0, 6.0, 8.0, 10.0]

Index Access with __getitem__ and __setitem__

Section titled “Index Access with __getitem__ and __setitem__”

Make your objects work like lists or dictionaries:

index_access.py
class ShoppingCart:
"""Shopping cart that supports index access"""
def __init__(self):
self.items = []
def add_item(self, item: str):
self.items.append(item)
def __getitem__(self, index):
"""Enable: cart[0]"""
return self.items[index]
def __setitem__(self, index, value):
"""Enable: cart[0] = 'new_item'"""
self.items[index] = value
def __len__(self):
"""Enable: len(cart)"""
return len(self.items)
def __delitem__(self, index):
"""Enable: del cart[0]"""
del self.items[index]
cart = ShoppingCart()
cart.add_item("Laptop")
cart.add_item("Mouse")
print(cart[0]) # "Laptop" - uses __getitem__
cart[1] = "Keyboard" # Uses __setitem__
print(cart[1]) # "Keyboard"
print(len(cart)) # 2 - uses __len__
del cart[0] # Uses __delitem__
print(len(cart)) # 1

Implement rich comparison methods:

comparison_operators.py
class Money:
"""Money class with comparison operators"""
def __init__(self, amount: float, currency: str = "USD"):
self.amount = amount
self.currency = currency
def __eq__(self, other):
"""== operator"""
if isinstance(other, Money):
return self.amount == other.amount and self.currency == other.currency
return False
def __lt__(self, other):
"""< operator"""
if isinstance(other, Money) and self.currency == other.currency:
return self.amount < other.amount
return NotImplemented
def __le__(self, other):
"""<= operator"""
return self < other or self == other
def __gt__(self, other):
"""> operator"""
if isinstance(other, Money) and self.currency == other.currency:
return self.amount > other.amount
return NotImplemented
def __ge__(self, other):
""">= operator"""
return self > other or self == other
def __repr__(self):
return f"Money({self.amount}, '{self.currency}')"
m1 = Money(100.0)
m2 = Money(50.0)
m3 = Money(100.0)
print(m1 > m2) # True
print(m1 < m2) # False
print(m1 >= m3) # True
print(m1 <= m2) # False

Control how your objects are displayed:

string_representations.py
class Product:
"""Product with custom string representations"""
def __init__(self, name: str, price: float, sku: str):
self.name = name
self.price = price
self.sku = sku
def __str__(self):
"""Informal string - for end users"""
return f"{self.name} - ${self.price:.2f}"
def __repr__(self):
"""Official string - for developers/debugging"""
return f"Product(name='{self.name}', price={self.price}, sku='{self.sku}')"
def __format__(self, format_spec):
"""Custom formatting with f-strings"""
if format_spec == "short":
return self.name
elif format_spec == "price":
return f"${self.price:.2f}"
return str(self)
product = Product("Laptop", 999.99, "LAP-001")
print(str(product)) # "Laptop - $999.99"
print(repr(product)) # "Product(name='Laptop', price=999.99, sku='LAP-001')"
print(f"{product:short}") # "Laptop"
print(f"{product:price}") # "$999.99"
CategoryMethodPurposeExample
Initialization__init__Object creationobj = MyClass()
String__str__User-friendly stringstr(obj)
__repr__Developer stringrepr(obj)
__format__Custom formattingf"{obj:format}"
Arithmetic__add__Additionobj1 + obj2
__sub__Subtractionobj1 - obj2
__mul__Multiplicationobj1 * obj2
__truediv__Divisionobj1 / obj2
__mod__Moduloobj1 % obj2
Comparison__eq__Equalityobj1 == obj2
__lt__Less thanobj1 < obj2
__le__Less or equalobj1 <= obj2
__gt__Greater thanobj1 > obj2
__ge__Greater or equalobj1 >= obj2
Container__len__Lengthlen(obj)
__getitem__Index accessobj[key]
__setitem__Index assignmentobj[key] = val
__delitem__Index deletiondel obj[key]
__contains__Membershipitem in obj
Iteration__iter__Iteratorfor item in obj
__next__Next itemnext(iterator)
Callable__call__Make callableobj()
Context__enter__Context entrywith obj:
__exit__Context exitwith obj:
Attributes__getattr__Missing attributeobj.missing
__setattr__Attribute setobj.attr = val
__delattr__Attribute deletedel obj.attr