0%

39- Dunder (Magic) Methods

Special methods with double underscores. They make your objects work with Python’s built-in operations. Add, subtract, compare, iterate, and more.

In Python, everything is an object. Strings have methods. Lists have methods. Numbers have methods. Even the + operator is actually a method call. Dunder methods (short for “double underscore”) are special methods that Python calls automatically in certain situations. They are surrounded by double underscores on both sides, like __init__, __str__, and __add__. When you write x + y, Python looks for an __add__ method on x. When you write len(x), Python looks for __len__. When you write x == y, Python looks for __eq__. By implementing these methods in your own classes, you can make your objects behave like built-in types. You can define what addition means for your class. You can make your objects iterable. You can control how they are compared. You can even make them callable like functions.

🕯️ 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.

What Are Dunder Methods?
Dunder methods are special methods that Python calls implicitly. They are not meant to be called directly by your code.

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 methods are sometimes called “magic methods” because they add magic behavior to your objects. However, the Python community prefers the term “dunder methods” (double underscore methods).
Common Dunder Methods at a Glance
Dunder MethodWhen Python Calls ItYour Code
__init__(self)When an object is createdobj = Class()
__str__(self)When print() or str() is usedprint(obj)
__repr__(self)When repr() is used or in debuggerrepr(obj)
__len__(self)When len() is calledlen(obj)
__getitem__(self, key)When using indexing obj[key]obj[0]
__setitem__(self, key, value)When setting value by indexobj[0] = 5
__contains__(self, item)When using in operatoritem in obj
__call__(self, *args)When object is called like a functionobj()
Constructor: __init__
You already know __init__. It initializes a new object after it is created. It is the most common dunder method.

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

String Representation: __str__ and __repr__
__str__ is for users (readable). __repr__ is for developers (unambiguous, often recreatable).

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().

Length: __len__
Implement __len__ to make your objects work with the len() function.

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

Container Methods: __getitem__, __setitem__, __delitem__
Make your object work like a list or dictionary with indexing.

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

Membership: __contains__
Implement __contains__ to support the in operator.

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

Callable Objects: __call__
Make your object callable like a function using __call__.

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.

Arithmetic Operators
Implement arithmetic operators to make your objects work with +, , *, etc.
OperatorMethodReverse MethodIn-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__)

Comparison Operators
Implement comparison methods to make your objects comparable with ==, <, >, etc.
OperatorMethod
==__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.

Iteration: __iter__ and __next__
Make your objects iterable with __iter__ and __next__.

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

Context Manager: __enter__ and __exit__
Make your objects work with the with statement.

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

Complete Example: A Custom Range Class
Combine multiple dunder methods to create a powerful custom class.

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)

Common Mistakes with Dunder Methods
  • 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
Check Your Understanding
  • 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.

Related posts