devnotes.

Dunder Methods and Operator Overloading

Dunder methods ("double underscore," like __init__) let a class customize how built-in operators and functions behave with its instances — Python's mechanism for operator overloading.

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

    def __add__(self, other):          # defines what + does for Vector + Vector
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other):            # defines what == does
        return self.x == other.x and self.y == other.y

    def __repr__(self):                  # defines what print()/repr() shows
        return f"Vector({self.x}, {self.y})"

    def __len__(self):                    # defines what len() returns
        return int((self.x ** 2 + self.y ** 2) ** 0.5)

v1, v2 = Vector(1, 2), Vector(3, 4)
print(v1 + v2)             # Vector(4, 6) — calls __add__, then __repr__ to display it
print(v1 == Vector(1, 2))    # True — calls __eq__

__init__ (constructor) and __repr__ (how an object prints) are the two dunder methods you'll write the most often — without __repr__, print(v1) would show something unhelpful like <__main__.Vector object at 0x...> instead of a readable representation.

Other common dunder methods: __str__ (used by str()/print() specifically, falls back to __repr__ if absent), __len__ (used by len()), __getitem__ (enables obj[key] indexing), __iter__ (makes an object iterable in a for loop), and __call__ (lets an instance be called like a function, instance()). Nearly every "magic" behavior of built-in types — why [1,2] + [3,4] concatenates, why len("abc") works, why for x in my_list: iterates — is implemented via exactly these same dunder methods on the built-in types themselves, meaning custom classes can participate in the same protocols as built-ins by implementing the matching dunders.

Dunder Methods and Operator Overloading in Python | DevNotes