Python Tutorial
Magic Methods and Operator Overloading
Magic methods — also called dunder methods because of their double underscores — let your classes integrate with Python's syntax and built-ins. Define __str__ and print() shows something useful; define __add__ and + works on your objects; define __len__, __getitem__ and __iter__ and your class behaves like a built-in container.
This lesson covers string representations, comparison and hashing, arithmetic operator overloading, container and iteration methods, making objects callable, and the context-manager methods.
String Representations
__repr__ returns an unambiguous, developer-facing representation (ideally valid code to recreate the object) used by the REPL, debuggers and containers. __str__ returns a friendly representation for print() and str(); it falls back to __repr__. __format__ supports format specs in f-strings.
Comparison and Hashing
__eq__, __lt__, __le__, __gt__, __ge__ and __ne__ define comparisons; functools.total_ordering fills in the rest from __eq__ and one ordering method. If you define __eq__, define __hash__ consistently (equal objects must have equal hashes) — or leave objects unhashable if they are mutable.
Arithmetic Operators
__add__ (+), __sub__ (-), __mul__ (*), __truediv__ (/), __floordiv__, __mod__, __pow__, __neg__, __abs__. Reflected versions (__radd__, __rmul__) handle cases like 3 * vector, and in-place versions (__iadd__) handle +=. Return NotImplemented for unsupported types so Python can try the other operand.
Containers, Iteration, Calling and Context Managers
__len__, __getitem__, __setitem__, __delitem__, __contains__ and __iter__ make a class behave like a collection. __bool__ controls truthiness. __call__ makes instances callable like functions. __enter__ and __exit__ make objects usable in with statements.
Examples
__repr__, __str__ and __format__
class Money:
def __init__(self, amount, currency="INR"):
self.amount, self.currency = amount, currency
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
def __str__(self):
return f"{self.currency} {self.amount:,.2f}"
def __format__(self, spec):
return f"{self.amount:{spec}} {self.currency}" if spec else str(self)
m = Money(1234.5)
print(m)
print(repr(m))
print([m])
print(f"{m:.0f}")
INR 1,234.50
Money(1234.5, 'INR')
[Money(1234.5, 'INR')]
1234 INR
Operator overloading for a Vector class
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
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, k):
return Vector(self.x * k, self.y * k)
__rmul__ = __mul__ # supports 3 * v
def __neg__(self):
return Vector(-self.x, -self.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __eq__(self, other):
return isinstance(other, Vector) and (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
def __bool__(self):
return bool(self.x or self.y)
a, b = Vector(3, 4), Vector(1, 2)
print(a + b, a - b, a * 2, 3 * b, -a, abs(a))
print(a == Vector(3, 4), bool(Vector(0, 0)), len({a, Vector(3, 4)}))
try:
a + 5
except TypeError as e:
print("TypeError:", e)
Vector(4, 6) Vector(2, 2) Vector(6, 8) Vector(3, 6) Vector(-3, -4) 5.0
True False 1
TypeError: unsupported operand type(s) for +: 'Vector' and 'int'
Comparisons with total_ordering, and sorting objects
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, text):
self.parts = tuple(int(p) for p in text.split("."))
def __eq__(self, other):
return self.parts == other.parts
def __lt__(self, other):
return self.parts < other.parts
def __repr__(self):
return ".".join(map(str, self.parts))
versions = [Version("3.12.1"), Version("3.9.0"), Version("3.12.0"), Version("3.10.4")]
print(sorted(versions))
print(Version("3.10.0") > Version("3.9.9"), Version("1.0") >= Version("1.0"), max(versions))
[3.9.0, 3.10.4, 3.12.0, 3.12.1]
True True 3.12.1
A container class, a callable object and a context manager
class Inventory:
def __init__(self):
self._items = {}
def __setitem__(self, name, qty):
self._items[name] = qty
def __getitem__(self, name):
return self._items.get(name, 0)
def __delitem__(self, name):
del self._items[name]
def __contains__(self, name):
return name in self._items
def __len__(self):
return len(self._items)
def __iter__(self):
return iter(sorted(self._items))
inv = Inventory()
inv["pens"] = 40; inv["books"] = 5; inv["bags"] = 2
del inv["bags"]
print(len(inv), inv["pens"], inv["lamps"], "books" in inv, list(inv))
class Discount:
def __init__(self, percent):
self.percent = percent
def __call__(self, price):
return price * (100 - self.percent) / 100
festive = Discount(20)
print(festive(500), callable(festive))
class Timer:
def __enter__(self):
print("start")
return self
def __exit__(self, exc_type, exc, tb):
print("stop, error:", exc_type.__name__ if exc_type else None)
return False
with Timer():
print("working")
2 40 0 True ['books', 'pens']
400.0 True
start
working
stop, error: None
Common Mistakes
- Defining __eq__ but not __hash__ for immutable value objects (Python makes them unhashable).
- Raising TypeError instead of returning NotImplemented from arithmetic methods.
- Making __repr__ vague ("<Vector>") so debugging output is useless.
- Overloading operators with surprising meanings (e.g. + that deletes data).
Key Points to Remember
- __repr__ for developers, __str__ for users, __format__ for format specs.
- Comparison methods plus @total_ordering make objects sortable; keep __hash__ consistent with __eq__.
- Arithmetic dunders (plus __r*__ and __i*__ variants) enable operator overloading.
- __len__, __getitem__, __contains__, __iter__ make container-like classes.
- __call__ makes callable objects; __enter__/__exit__ make context managers.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.