AiTechWorlds
AiTechWorlds
When you write 2 + 3 in Python, you might think the + symbol is a built-in operation baked into the language. It is not. Python secretly translates 2 + 3 into int.__add__(2, 3). The + symbol is just syntactic sugar for a method call.
The same is true for every operator:
"hello" + " world" → str.__add__("hello", " world")len([1, 2, 3]) → list.__len__([1, 2, 3])x == y → x.__eq__(y)x[0] → x.__getitem__(0)These special methods all have double underscores on both sides. The Python community affectionately calls them dunder methods (double underscore). When you define them on your own classes, Python calls them automatically in response to operators and built-in functions.
You don't call
v.__add__(other)directly. You writev + otherand Python does the translation for you.
Without magic methods, every class would need its own named methods: vector.add(other), vector.compare(other), vector.to_string(). Code using your class would look awkward and inconsistent.
With magic methods, your class feels native to Python. Users can write v1 + v2, abs(v), len(v), print(v) — exactly as they would with built-in types. The class fits naturally into the language.
| Category | Method | Triggered By |
|---|---|---|
| Arithmetic | __add__, __sub__, __mul__, __truediv__ | +, -, *, / |
| Comparison | __eq__, __lt__, __gt__, __le__, __ge__ | ==, <, >, <=, >= |
| Representation | __str__, __repr__ | print(), repr(), f-strings |
| Container | __len__, __getitem__, __contains__, __iter__ | len(), [], in, for loops |
| Context Manager | __enter__, __exit__ | with statement |
| Object Lifecycle | __init__, __del__ | creation, garbage collection |
Let's build a 2D vector class that feels completely natural to use.
import math # For square root in magnitude calculation
class Vector2D:
def __init__(self, x: float, y: float):
self.x = x # Horizontal component
self.y = y # Vertical component
# --- Representation ---
def __repr__(self) -> str:
# Used in the REPL and for debugging — should be unambiguous
return f"Vector2D({self.x}, {self.y})"
def __str__(self) -> str:
# Used by print() and str() — human-friendly format
return f"({self.x}, {self.y})"
# --- Arithmetic Operators ---
def __add__(self, other: "Vector2D") -> "Vector2D":
# v1 + v2 → add corresponding components
return Vector2D(self.x + other.x, self.y + other.y)
def __sub__(self, other: "Vector2D") -> "Vector2D":
# v1 - v2 → subtract corresponding components
return Vector2D(self.x - other.x, self.y - other.y)
def __mul__(self, scalar: float) -> "Vector2D":
# v * 3 → scale both components by the scalar
return Vector2D(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar: float) -> "Vector2D":
# 3 * v → Python tries scalar.__mul__(v) first, then v.__rmul__(scalar)
# Without __rmul__, "3 * v" would fail even though "v * 3" works
return self.__mul__(scalar)
def __truediv__(self, scalar: float) -> "Vector2D":
# v / 2 → divide both components
if scalar == 0:
raise ValueError("Cannot divide a vector by zero")
return Vector2D(self.x / scalar, self.y / scalar)
def __neg__(self) -> "Vector2D":
# -v → flip the sign of both components
return Vector2D(-self.x, -self.y)
# --- Comparison Operators ---
def __eq__(self, other: object) -> bool:
# v1 == v2 → compare both components
if not isinstance(other, Vector2D):
return NotImplemented # Let Python handle incompatible types
return self.x == other.x and self.y == other.y
def __lt__(self, other: "Vector2D") -> bool:
# v1 < v2 → compare magnitudes (shorter vector is "less than")
return abs(self) < abs(other)
def __le__(self, other: "Vector2D") -> bool:
return abs(self) <= abs(other)
def __gt__(self, other: "Vector2D") -> bool:
return abs(self) > abs(other)
def __ge__(self, other: "Vector2D") -> bool:
return abs(self) >= abs(other)
# --- Built-in Function Support ---
def __abs__(self) -> float:
# abs(v) → magnitude (length) of the vector using Pythagorean theorem
return math.sqrt(self.x ** 2 + self.y ** 2)
def __len__(self) -> int:
# len(v) → number of dimensions (always 2 for Vector2D)
return 2
def __bool__(self) -> bool:
# bool(v) → False only for the zero vector (0, 0)
return self.x != 0 or self.y != 0
# --- Container-style Access ---
def __getitem__(self, index: int) -> float:
# v[0] → x component, v[1] → y component
if index == 0:
return self.x
elif index == 1:
return self.y
raise IndexError(f"Vector2D index {index} out of range (0 or 1)")
def __iter__(self):
# for component in v → yield x then y
yield self.x
yield self.y
def __contains__(self, value: float) -> bool:
# 3.0 in v → True if 3.0 is either component
return value == self.x or value == self.y
Using the Vector2D class:
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
print(v1) # (3, 4) — calls __str__
print(repr(v1)) # Vector2D(3, 4) — calls __repr__
print(v1 + v2) # (4, 6) — calls __add__
print(v1 - v2) # (2, 2) — calls __sub__
print(v1 * 3) # (9, 12) — calls __mul__
print(2 * v1) # (6, 8) — calls __rmul__
print(v1 / 2) # (1.5, 2.0) — calls __truediv__
print(-v1) # (-3, -4) — calls __neg__
print(abs(v1)) # 5.0 — calls __abs__ (3-4-5 right triangle)
print(len(v1)) # 2 — calls __len__
print(bool(v1)) # True — calls __bool__
print(v1 == Vector2D(3, 4)) # True — calls __eq__
print(v1 > v2) # True — calls __gt__ (magnitude 5.0 > 2.24)
print(v1[0]) # 3 — calls __getitem__
print(list(v1)) # [3, 4] — calls __iter__
print(3 in v1) # True — calls __contains__
__enter__ and __exit__class ManagedFile:
def __init__(self, filename: str, mode: str):
self.filename = filename # File path to open
self.mode = mode # Read/write mode
def __enter__(self):
# Called when entering the `with` block — open the resource
self.file = open(self.filename, self.mode)
return self.file # Assigned to the `as` variable
def __exit__(self, exc_type, exc_val, exc_tb):
# Called when leaving the `with` block — always runs, even on error
self.file.close()
return False # False means: don't suppress exceptions
# Usage:
with ManagedFile("data.txt", "w") as f:
f.write("Hello, world!")
# File is guaranteed to be closed here, even if an exception occurred
__str__ is for humans (readable), __repr__ is for developers (unambiguous).__rmul__ handles the case where your object is on the right side of an operator.NotImplemented (not False) when a comparison doesn't make sense — Python will try the other operand.__enter__ and __exit__ power the with statement, guaranteeing resource cleanup.When your class defines
__add__, users writev1 + v2— the same way they write2 + 3. Your class becomes part of the language.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises