🕯️ Magic Note
Dunder methods are never meant to be called directly. You do not write obj.__add__(other). You write obj + other. Python translates the operator into the dunder method call. This is called operator overloading, and it is one of Python’s most distinctive features.
Python
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f”{self.name} ({self.age})”
p = Person(“Ali”, 25)
print(p) # Python automatically calls p.__str__()
# Output: Ali (25)
| Dunder Method | When Python Calls It | Your Code |
|---|---|---|
| __init__(self) | When an object is created | obj = Class() |
| __str__(self) | When print() or str() is used | print(obj) |
| __repr__(self) | When repr() is used or in debugger | repr(obj) |
| __len__(self) | When len() is called | len(obj) |
| __getitem__(self, key) | When using indexing obj[key] | obj[0] |
| __setitem__(self, key, value) | When setting value by index | obj[0] = 5 |
| __contains__(self, item) | When using in operator | item in obj |
| __call__(self, *args) | When object is called like a function | obj() |
Python
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
self.current_page = 1
book = Book(“The Python Magic”, “Feloriya”, 300)
print(book.title) # The Python Magic
Python
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __str__(self):
# User-friendly representation
return f”{self.celsius}°C”
def __repr__(self):
# Developer-friendly, should be unambiguous
return f”Temperature({self.celsius})”
t = Temperature(25)
print(str(t)) # 25°C (uses __str__)
print(repr(t)) # Temperature(25) (uses __repr__)
print(t) # 25°C (print uses __str__)
🕯️ Magic Note
If you define only __repr__, it will be used for both repr() and str(). It is good practice to define both. The __repr__ should ideally return a string that could recreate the object when passed to eval().
Python
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __len__(self):
return len(self.songs)
playlist = Playlist(“My Favorites”)
playlist.add_song(“Song A”)
playlist.add_song(“Song B”)
playlist.add_song(“Song C”)
print(len(playlist)) # 3
Python
class CustomList:
def __init__(self, items):
self._items = list(items)
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, value):
self._items[index] = value
def __delitem__(self, index):
del self._items[index]
def __len__(self):
return len(self._items)
my_list = CustomList([10, 20, 30, 40])
print(my_list[1]) # 20 (__getitem__)
my_list[2] = 99 # (__setitem__)
print(my_list[2]) # 99
del my_list[0] # (__delitem__)
print(len(my_list)) # 3
Python
class ShoppingCart:
def __init__(self):
self.items = {}
def add_item(self, item, quantity=1):
self.items[item] = self.items.get(item, 0) + quantity
def __contains__(self, item):
return item in self.items
cart = ShoppingCart()
cart.add_item(“apple”, 3)
cart.add_item(“banana”, 2)
print(“apple” in cart) # True
print(“orange” in cart) # False
Python
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10 (calls __call__)
print(triple(5)) # 15
print(callable(double)) # True
🕯️ Magic Note
The __call__ method allows you to create function-like objects that can maintain state between calls. This is useful for decorators, callbacks, and function factories.
| Operator | Method | Reverse Method | In-place Method |
|---|---|---|---|
| + | __add__(self, other) | __radd__(self, other) | __iadd__(self, other) |
| – | __sub__(self, other) | __rsub__(self, other) | __isub__(self, other) |
| * | __mul__(self, other) | __rmul__(self, other) | __imul__(self, other) |
| / | __truediv__(self, other) | __rtruediv__(self, other) | __itruediv__(self, other) |
| // | __floordiv__(self, other) | __rfloordiv__(self, other) | __ifloordiv__(self, other) |
| % | __mod__(self, other) | __rmod__(self, other) | __imod__(self, other) |
| ** | __pow__(self, other) | __rpow__(self, other) | __ipow__(self, other) |
Python
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 __rmul__(self, scalar):
# For scalar * vector (when scalar is on the left)
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f”Vector({self.x}, {self.y})”
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2) # Vector(6, 8)
print(v1 – v2) # Vector(-2, -2)
print(v1 * 3) # Vector(6, 9)
print(3 * v1) # Vector(6, 9) (__rmul__)
| Operator | Method |
|---|---|
| == | __eq__(self, other) |
| != | __ne__(self, other) (if not defined, defaults to not __eq__) |
| < | __lt__(self, other) |
| <= | __le__(self, other) |
| > | __gt__(self, other) |
| >= | __ge__(self, other) |
Python
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __eq__(self, other):
return self.score == other.score
def __lt__(self, other):
return self.score < other.score
def __le__(self, other):
return self.score <= other.score
def __str__(self):
return f”{self.name}: {self.score}”
s1 = Student(“Ali”, 85)
s2 = Student(“Sara”, 92)
s3 = Student(“Reza”, 85)
print(s1 == s3) # True (same score)
print(s1 < s2) # True (85 < 92)
print(s2 > s1) # True (92 > 85)
students = [s1, s2, s3]
students.sort() # Works because __lt__ is defined
for s in students:
print(s)
🕯️ Magic Note
If you define __eq__ and __lt__ but not __ne__, Python automatically uses not __eq__ for != . The functools.total_ordering decorator can fill in missing comparison methods if you define __eq__ and one other.
Python
class CountDown:
def __init__(self, start):
self.start = start
def __iter__(self):
return self
def __next__(self):
if self.start <= 0:
raise StopIteration
self.start -= 1
return self.start + 1
for num in CountDown(5):
print(num, end=” “)
# 5 4 3 2 1
Python
class ManagedFile:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
# Usage
with ManagedFile(“test.txt”, “w”) as f:
f.write(“Hello, World!”)
# File is automatically closed
Python
class CustomRange:
def __init__(self, start, stop, step=1):
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
self.current = self.start
return self
def __next__(self):
if (self.step > 0 and self.current >= self.stop) or (self.step < 0 and self.current <= self.stop):
raise StopIteration
value = self.current
self.current += self.step
return value
def __len__(self):
# Number of steps
return max(0, (self.stop – self.start + self.step – 1) // self.step) if self.step > 0 else max(0, (self.start – self.stop – self.step – 1) // -self.step)
def __contains__(self, x):
if self.step > 0:
return self.start <= x < self.stop and (x – self.start) % self.step == 0
else:
return self.stop <= x <= self.start and (x – self.start) % self.step == 0
def __getitem__(self, index):
if index < 0:
index += len(self)
if index < 0 or index >= len(self):
raise IndexError(“Index out of range”)
return self.start + index * self.step
def __str__(self):
return f”CustomRange({self.start}, {self.stop}, {self.step})”
# Using the class
r = CustomRange(1, 10, 2)
print(list(r)) # [1, 3, 5, 7, 9]
print(len(r)) # 5
print(5 in r) # True
print(4 in r) # False
print(r[0]) # 1
print(r[-1]) # 9
print(r) # CustomRange(1, 10, 2)
- Calling dunder methods directly (write len(obj), not obj.__len__())
- Forgetting to return NotImplemented for unsupported operations (returns None instead)
- Forgetting to raise StopIteration in __next__ (causes infinite loops)
- Not returning self from __iter__ when implementing iterator yourself
- Implementing __len__ but forgetting to make it return a non-negative integer
- What is the difference between __str__ and __repr__?
- Write a class Fraction that implements __add__ and __str__.
- How do you make an object callable like a function?
- What method do you implement to support the len() function?
- Write a class EvenNumbers that is iterable and yields even numbers up to a limit.
- What is the purpose of __enter__ and __exit__?
⚡ Whisper
Dunder methods are the secret language between your objects and Python itself. You do not call them. Python calls them when you use operators, functions, and syntax. When you write x + y, Python whispers x.__add__(y). When you write len(x), Python asks x.__len__(). When you write x in y, Python searches y.__contains__(x). By implementing these methods, you tell Python how your objects should behave. You can make a vector that adds like a vector. A playlist that measures its length. A counter that calls like a function. You are not just using Python. You are becoming part of it. Your objects become first-class citizens, indistinguishable from built-in types. This is the ultimate integration. This is the dunder path. Walk it, and your objects will speak the language of Python fluently.