PythonDunder / Magic Methods

Dunder / Magic Methods

"Dunder" is short for double underscore. Dunder methods (also called magic methods or special methods) are methods with names like __init__, __str__, and __add__ — surrounded on both sides by double underscores. Python calls these methods automatically in response to built-in syntax and built-in functions, instead of you having to call them by name yourself.

This is what lets your own classes plug directly into Python's native syntax. Instead of inventing a method called add_vectors and forcing everyone to call v1.add_vectors(v2), you implement __add__, and suddenly plain v1 + v2 just works. Instead of a custom to_display_string method, you implement __str__, and print(obj) and str(obj) just work. Dunder methods are the glue between your objects and Python's operators, len(), indexing, iteration, comparisons, and more.

Note
You'll almost never call a dunder method directly, like `v1.__add__(v2)`. You define it, and Python calls it for you when the corresponding syntax or built-in function is used.
`__str__` vs `__repr__`

Two of the most common dunder methods control how an object turns into text, but they serve different audiences. __str__ is meant to produce a readable, user-friendly description — it's what print() and str() use. __repr__ is meant to produce an unambiguous, developer-facing representation, ideally one that looks like valid Python code that could recreate the object. It's what the interactive REPL shows, and it's also what's used when displaying objects inside containers like lists.

Method

Called By

Purpose

Fallback Behavior

__str__

print(obj), str(obj)

Readable, user-facing description

Falls back to __repr__ if not defined

__repr__

REPL echo, repr(obj), objects inside lists/dicts

Unambiguous, debug-friendly representation

Falls back to default like <Point object at 0x...> if not defined

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

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

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


p = Point(3, 4)
print(p)          # uses __str__
print([p])        # uses __repr__ for the item inside the list
print(repr(p))    # uses __repr__ directly
(3, 4)
[Point(x=3, y=4)]
Point(x=3, y=4)
Tip
If you only define one of the two, define `__repr__`. Python automatically falls back to `__repr__` when `__str__` is missing, so a single good `__repr__` covers both cases reasonably well. Defining only `__str__` doesn't help `repr()` at all.
`__eq__` and `__lt__`: Comparisons

By default, Python compares custom objects by identity — two separate instances are never considered equal, even with identical data. __eq__ lets you define what "equal to" means for your class, controlling the == operator. __lt__ defines "less than," controlling the less than operator, and it's the method Python's sorting functions (sorted(), .sort()) rely on to decide ordering.

Python
class Money:
    def __init__(self, amount):
        self.amount = amount

    def __eq__(self, other):
        return self.amount == other.amount

    def __lt__(self, other):
        return self.amount < other.amount

    def __repr__(self):
        return f"Money({self.amount})"


wallet = [Money(50), Money(5), Money(20)]

print(Money(10) == Money(10))   # uses __eq__
print(sorted(wallet))           # uses __lt__ to order the list
True
[Money(5), Money(20), Money(50)]

Without __eq__, Money(10) == Money(10) would return False, because Python would be comparing two distinct objects by identity rather than by their amount values. Without __lt__, calling sorted() on a list of Money objects would raise a TypeError, since Python wouldn't know how to decide which one comes first.

`__len__` and `__getitem__`

__len__ lets your object respond to the built-in len() function. __getitem__ lets your object support square-bracket indexing, obj[i], which is what makes a custom class feel like a real collection. Here's a small custom collection that implements both:

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

    def __len__(self):
        return len(self.songs)

    def __getitem__(self, index):
        return self.songs[index]


playlist = Playlist(["Intro", "Solo", "Outro"])

print(len(playlist))       # uses __len__
print(playlist[0])         # uses __getitem__
print(playlist[-1])        # negative indexing works too

for song in playlist:      # iteration works via repeated __getitem__ calls
    print(song)
3
Intro
Outro
Intro
Solo
Outro

The for loop at the end works even though Playlist never defined an __iter__ method or anything iteration-specific. As a bonus effect of implementing __getitem__, Python falls back to calling it with 0, 1, 2, and so on until it hits an IndexError, which is enough to make the object iterable for free.

`__add__`: A Full Operator Overloading Example

__add__ controls what the + operator does with your objects, letting you overload it with behavior that makes sense for your class. Here's a Vector class that supports adding two vectors together with plain + syntax:

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

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

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


v1 = Vector(2, 3)
v2 = Vector(4, 1)

v3 = v1 + v2   # Python calls v1.__add__(v2) behind the scenes
print(v3)
Vector(6, 4)

Writing v1 + v2 triggers Python to call v1.__add__(v2) automatically. That method returns a brand-new Vector built from the sum of each vector's x and y components, and __repr__ makes the result print cleanly instead of showing a generic object address. Without __add__ defined, v1 + v2 would raise a TypeError telling you the + operator isn't supported between two Vector instances.

  • __str__ / __repr__ — control how an object is displayed as text.

  • __eq__ / __lt__ — control == and less than, and enable sorting.

  • __len__ / __getitem__ — support len() and obj[index], with iteration as a side effect of the latter.

  • __add__ — and its many siblings like __sub__, __mul__ — control arithmetic operators.

Note
This page only scratches the surface. Python defines dozens of dunder methods covering everything from context managers (`__enter__` / `__exit__`) to callable objects (`__call__`) to attribute access (`__getattr__`). The pattern is always the same: Python built-in syntax on the left, a dunder method on your class on the right.