Course topics

By WebNest Studio

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__

Python
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}")
Output
INR 1,234.50
Money(1234.5, 'INR')
[Money(1234.5, 'INR')]
1234 INR

Operator overloading for a Vector class

Python
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)
Output
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

Python
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))
Output
[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

Python
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")
Output
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.