Python
Magic Methods in Python
Intermediate
Magic Methods in Python: The Secret Sauce Behind Elegant Code
Ever wondered why you can write
vector1 + vector2
,
if user in users
, or
len(my_custom_list)
and it just works beautifully? The answer is magic methods (also called dunder methods). These special methods let you define how your objects interact with Python’s built-in operators, functions, and syntax. They are the reason Python code can feel so natural and expressive. In this guide, we’ll explore the most important magic methods, from comparisons and arithmetic to containers and beyond, with clear explanations and practical, ready-to-use examples.
Magic Methods in Python
Why they matter
- They let you customize the behavior of your classes so they feel like built-in Python types.
- They enable operator overloading (e.g., +, *, ==, [], etc.).
- They hook into Python's core protocols (iteration, context managers, async, etc.).
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # Vector + Vector
return Vector(self.x + other.x, self.y + other.y)
def __str__(self): # print(vector) or str(vector)
return f"Vector({self.x}, {self.y})"
def __len__(self): # len(vector)
return 2
#With these methods, you can write natural, readable code:
v1 = Vector(2, 3)
v2 = Vector(5, 7)
print(v1 + v2) # Vector(7, 10)
print(len(v1)) # 2
Comparison Magic Methods
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
def __ne__(self, other):
return not self == other
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
def __le__(self, other):
return self < other or self == other
def __gt__(self, other):
return not (self <= other)
def __ge__(self, other):
return not (self < other)
# Usage
v1 = Version(3, 8, 5)
v2 = Version(3, 9, 0)
print(v1 == v2) # False
print(v1 < v2) # True
print(v1 <= v2) # True
print(v1 > v2) # False
print(v1 >= v2) # False
print(v1 != v2) # True
Normal arithmetic operators
class Number:
def __init__(self, value):
self.value = value
def __str__(self):
return str(self.value)
# Arithmetic operators
def __add__(self, other):
return Number(self.value + other.value)
def __sub__(self, other):
return Number(self.value - other.value)
def __mul__(self, other):
return Number(self.value * other.value)
def __truediv__(self, other):
return Number(self.value / other.value)
def __floordiv__(self, other):
return Number(self.value // other.value)
def __mod__(self, other):
return Number(self.value % other.value)
def __divmod__(self, other):
return divmod(self.value, other.value)
def __pow__(self, other):
return Number(self.value ** other.value)
# Bitwise operators
def __lshift__(self, other):
return Number(self.value << other.value)
def __rshift__(self, other):
return Number(self.value >> other.value)
def __and__(self, other):
return Number(self.value & other.value)
def __or__(self, other):
return Number(self.value | other.value)
def __xor__(self, other):
return Number(self.value ^ other.value)
# Usage
a = Number(10)
b = Number(3)
print(a + b) # 13
print(a - b) # 7
print(a * b) # 30
print(a / b) # 3.333...
print(a // b) # 3
print(a % b) # 1
print(divmod(a, b)) # (3, 1)
print(a ** b) # 1000
print(a << b) # 80
print(a >> b) # 1
print(a & b) # 2
print(a | b) # 11
print(a ^ b) # 9
Type conversion magic methods
import math
class Value:
def __init__(self, num):
self.num = num
def __str__(self):
return str(self.num)
# Type conversion methods
def __int__(self):
return int(self.num)
def __float__(self):
return float(self.num)
def __complex__(self):
return complex(self.num)
def __index__(self):
return int(self.num) # Used for slicing and base conversions
def __trunc__(self):
return math.trunc(self.num)
def __oct__(self):
return oct(int(self.num))
def __hex__(self):
return hex(int(self.num))
# Usage
v = Value(42.7)
print(int(v)) # 42
print(float(v)) # 42.7
print(complex(v)) # (42.7+0j)
print(hex(v)) # 0x2a
print(oct(v)) # 0o52
print(bin(v)) # 0b101010 (uses __index__)
print(math.trunc(v)) # 42
# Used in slicing
lst = ['a', 'b', 'c', 'd', 'e']
print(lst[v]) # 'e' (index 42 % len? Wait, actually uses __index__)
Augmented assignment
class Number:
def __init__(self, value):
self.value = value
def __str__(self):
return str(self.value)
# Augmented assignment methods (in-place)
def __iadd__(self, other):
self.value += other.value
return self
def __isub__(self, other):
self.value -= other.value
return self
def __imul__(self, other):
self.value *= other.value
return self
def __itruediv__(self, other):
self.value /= other.value
return self
def __ifloordiv__(self, other):
self.value //= other.value
return self
def __imod__(self, other):
self.value %= other.value
return self
def __ipow__(self, other):
self.value **= other.value
return self
def __ilshift__(self, other):
self.value <<= other.value
return self
def __irshift__(self, other):
self.value >>= other.value
return self
def __iand__(self, other):
self.value &= other.value
return self
def __ior__(self, other):
self.value |= other.value
return self
def __ixor__(self, other):
self.value ^= other.value
return self
# Usage
n = Number(10)
print("Original:", n)
n += Number(5) # 15
print("After += 5:", n)
n -= Number(3) # 12
print("After -= 3:", n)
n *= Number(2) # 24
print("After *= 2:", n)
n //= Number(5) # 4
print("After //= 5:", n)
n **= Number(3) # 64
print("After **= 3:", n)
n <<= Number(1) # 128
print("After <<= 1:", n)
Unary operators and functions
import math
class Number:
def __init__(self, value):
self.value = float(value) # store as float for flexibility
def __str__(self):
return str(self.value)
# Unary operators
def __pos__(self):
return Number(+self.value) # rarely used, but good to define
def __neg__(self):
return Number(-self.value)
def __abs__(self):
return Number(abs(self.value))
def __invert__(self):
return Number(~int(self.value)) # bitwise NOT (works on integers)
def __round__(self, ndigits=None):
return Number(round(self.value, ndigits))
def __floor__(self):
return Number(math.floor(self.value))
def __ceil__(self):
return Number(math.ceil(self.value))
def __trunc__(self):
return Number(math.trunc(self.value))
# Usage
n = Number(-42.7)
print("+n →", +n) # Unary positive
print("-n →", -n) # Negation
print("abs(n) →", abs(n)) # Absolute value
print("~n →", ~n) # Bitwise NOT (on int part)
print("round(n) →", round(n)) # -43
print("round(n, 1) →", round(n, 1)) # -42.7
print("math.floor(n)→", math.floor(n)) # -43
print("math.ceil(n) →", math.ceil(n)) # -42
print("math.trunc(n)→", math.trunc(n)) # -42
The magic behind containers
class CustomList:
def __init__(self, items=None):
self._items = list(items) if items is not None else []
def __str__(self):
return str(self._items)
def __len__(self):
return len(self._items)
def __bool__(self):
return len(self) > 0
def __getitem__(self, key):
if isinstance(key, slice):
return CustomList(self._items[key])
return self._items[key]
def __setitem__(self, key, value):
self._items[key] = value
def __delitem__(self, key):
del self._items[key]
def __iter__(self):
return iter(self._items) # Simple delegation
def __reversed__(self):
return reversed(self._items)
def __contains__(self, item):
return item in self._items
# Bonus: A dict-like example for __missing__
class SafeDict(dict):
def __missing__(self, key):
return f"<Missing: {key}>"
# Usage
fruits = CustomList(["apple", "banana", "cherry", "date"])
print(len(fruits)) # 4
print(bool(fruits)) # True
print(fruits[1]) # banana
print(fruits[1:3]) # CustomList(['banana', 'cherry'])
fruits[2] = "coconut"
del fruits[0]
print(fruits) # ['banana', 'coconut', 'date']
print("coconut" in fruits) # True
print("grape" in fruits) # False
# Iteration
for fruit in fruits:
print(fruit)
# Reversed
print(list(reversed(fruits))) # ['date', 'coconut', 'banana']
# Empty check
empty = CustomList()
print(bool(empty)) # False
# __missing__ example
d = CustomList.SafeDict(name="Alice")
print(d["name"]) # Alice
print(d["age"]) # <Missing: age>
Magic methods are one of Python’s most powerful features. They allow you to turn your custom classes into first-class citizens that behave just like built-in types.
Here’s what we covered:
Comparison operators** (`==`, `<`, `>`, etc.)
Arithmetic & bitwise operators** (`+`, `-`, `*`, `/`, `&`, `|`, etc.)
Augmented assignment** (`+=`, `*=` etc.)
Unary operators** (`-obj`, `abs()`, `~`)
Type conversions** (`int()`, `float()`, `round()`)
Container protocol** (`len()`, indexing, iteration, `in`, slicing)
By thoughtfully implementing these dunder methods, you can create intuitive, readable, and Pythonic APIs, whether you’re building domain-specific classes, data structures, or full libraries.
Final Tip:
Don’t overuse magic methods. Use them when they make your code
clearer
and more expressive, not just because you can.
Classes Magic Methods OOP Python