Magic methods — also called "dunder" methods because their names start and end with double underscores, like __init__ or __str__ — are how Python lets your own classes plug into the language's built-in behavior. They're what make len(my_object), print(my_object), or obj1 == obj2 work the way you want, instead of only working for Python's own built-in types.

You've already met one magic method if you've written a __init__ constructor — it's just the most commonly used one. There are dozens more, each hooking into a specific operation.

__str__ and __repr__: Controlling How an Object Prints

Without a __str__ method, printing an object gives you an unhelpful default like <__main__.Point object at 0x...>. Defining __str__ lets you control that output. __repr__ is similar but meant for a more unambiguous, developer-facing representation (shown in a REPL or inside a list of objects) — as a rule of thumb, __repr__ should ideally look like code that could recreate the object.

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])      # a list uses __repr__ for its items

Output:

(3, 4)
[Point(x=3, y=4)]

__len__ and __eq__

__len__ lets len() work on your own objects, and __eq__ lets you define what == means for them — by default, two objects are only "equal" if they're literally the same object in memory, which is rarely what you actually want for comparing data.

class Cart:
    def __init__(self, items):
        self.items = items

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

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

cart1 = Cart(["apple", "milk"])
cart2 = Cart(["apple", "milk"])

print(len(cart1))
print(cart1 == cart2)  # compares contents, not identity, thanks to __eq__

Output:

2
True

Other Common Magic Methods Worth Knowing

__lt__, __gt__, and similar methods let objects support comparison operators like < and > (useful for sorting custom objects). __add__ lets + work between your objects. __getitem__ lets an object support indexing with obj[0]. You don't need to implement all of these for every class — only add the ones that make your object behave naturally for how it's actually going to be used.

Once you're comfortable with magic methods, it's worth seeing how they relate to static methods and class methods, which are a different kind of special method behavior in Python classes.