Gyaan

Dunder (Magic) Methods

intermediate dunder magic-methods operator-overloading

Dunder methods (short for double underscore) are special methods that Python calls behind the scenes. They let us define how our objects behave with built-in operations like +, len(), print(), and even in.

Think of them like hooks — Python gives us specific spots to plug in custom behavior.

Object Basics

We’ve already seen __init__. Here are the other essentials:

class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __str__(self):          # print(book) — friendly output
        return f"'{self.title}' ({self.pages} pages)"

    def __repr__(self):         # repr(book) — developer output
        return f"Book('{self.title}', {self.pages})"

    def __len__(self):          # len(book)
        return self.pages

The only difference between __str__ and __repr__: __str__ is for humans, __repr__ is for developers. If only one is defined, Python falls back to __repr__ for everything.

Comparison Methods

These let us use ==, <, >, etc. with our objects.

class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __eq__(self, other):    # book1 == book2
        return self.pages == other.pages

    def __lt__(self, other):    # book1 < book2
        return self.pages < other.pages

    def __gt__(self, other):    # book1 > book2
        return self.pages > other.pages

Pro tip: if we define __eq__ and __lt__, we can use functools.total_ordering to auto-generate the rest (<=, >=).

Arithmetic Methods

We can make our objects work with +, *, and more.

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):   # v1 + v2
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):  # v1 * 3
        return Vector(self.x * scalar, self.y * scalar)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v = Vector(1, 2) + Vector(3, 4)
print(v)  # Vector(4, 6)

Container Methods

These make our objects behave like lists or dicts.

class Playlist:
    def __init__(self, songs):
        self._songs = songs

    def __getitem__(self, index):   # playlist[0]
        return self._songs[index]

    def __setitem__(self, index, value):  # playlist[0] = "new song"
        self._songs[index] = value

    def __contains__(self, item):   # "song" in playlist
        return item in self._songs

    def __len__(self):              # len(playlist)
        return len(self._songs)

Callable Objects

__call__ lets us use an object like a function.

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, value):
        return value * self.factor

double = Multiplier(2)
print(double(5))   # 10 — calling the object like a function

Context Manager Methods

__enter__ and __exit__ let our objects work with the with statement.

class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self

    def __exit__(self, *args):
        import time
        print(f"Elapsed: {time.time() - self.start:.2f}s")

with Timer():
    sum(range(1_000_000))  # Elapsed: 0.03s

__hash__

If we define __eq__, Python automatically sets __hash__ to None (making the object unhashable). To use our objects in sets or as dict keys, we need to define __hash__ too.

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))

In simple language, dunder methods are Python’s way of letting us teach our objects how to behave with built-in operations. Almost everything in Python — from + to len() to for loops — is powered by dunder methods under the hood.