Object-Oriented ProgrammingAdvanced9 min44 / 63

Magic (Dunder) Methods

Learn how double-underscore 'magic' methods let your custom objects behave just like Python built-ins — supporting print(), len(), +, ==, and more.

You already know that len([1, 2, 3]) returns 3, and that print(42) shows 42 in the console. But have you ever wondered — how does Python know what to do when you call len() on a list, or use + to add two numbers?

The answer is dunder methods (short for double-underscore methods, like __len__ or __add__). Python calls these special methods behind the scenes whenever you use a built-in operation. The exciting part: you can define them on your own classes so your objects feel just as natural as the built-ins.

See it in action

Visual walkthrough1 / 5
1

What Are Dunder Methods?

Dunder methods (double-underscore, like __str__) are special hooks Python calls automatically when you use built-in operations. Define them on your class and your objects behave just like Python's own built-ins.

"Dunder" = double underscore — `__like_this__`
Think of it like

Think of it like a universal remote

Python has a set of 'buttons' — +, len(), print(), ==. Dunder methods are like the wiring that connects those buttons to your object. Define __add__, and the + button now works with your class. You decide what happens when it's pressed.

#The Dunder You Already Know: __init__

Every class you write probably already uses one dunder method: __init__. This is called automatically when you create a new object. It sets up the object's starting state.

  • __init__ is not the constructor itself — Python already created the object. __init__ just initialises it.
  • The first parameter is always self, which refers to the new object being set up.
__init__ runs automatically when you write Money(50, 'USD')
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

wallet = Money(50, 'USD')
print(wallet.amount)
print(wallet.currency)

#Making print() Work: __str__

Try printing a plain object without any dunder methods and you get something ugly like <__main__.Money object at 0x10a3f2c40>. That's Python saying 'I have no idea how to show this nicely.'

Define __str__ and Python will call it whenever someone does print(obj) or str(obj). It must return a string.

__str__ controls what print() shows
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"

wallet = Money(50, 'USD')
print(wallet)

#The Developer View: __repr__

__repr__ is the developer-facing representation. It should ideally look like the code you'd write to recreate the object. Python uses __repr__ in the interactive console and as a fallback when __str__ is not defined.

  • str() → calls __str__ (user-friendly)
  • repr() → calls __repr__ (developer-friendly)
  • If only __repr__ is defined, str() uses it too.
__repr__ gives a precise, unambiguous description
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __repr__(self):
        return f"Money({self.amount!r}, {self.currency!r})"

wallet = Money(50, 'USD')
print(str(wallet))
print(repr(wallet))
Tip

Always define at least __repr__

If you only have time for one, write __repr__. It doubles as __str__ when needed, and it makes debugging much easier — you can see exactly what's inside your object at a glance.

#Supporting len(): __len__

When you call len(something), Python calls something.__len__() under the hood. Define it in your class and len() works on your objects too. It must return a non-negative integer.

__len__ lets built-in len() work on your class
class ShoppingCart:
    def __init__(self):
        self.items = []

    def add(self, item):
        self.items.append(item)

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

cart = ShoppingCart()
cart.add('apple')
cart.add('bread')
print(len(cart))

#Equality Checks: __eq__

By default, == checks whether two variables point to the exact same object in memory — not whether they have the same data. Define __eq__ to teach Python what 'equal' means for your class.

__eq__ controls what == means for your objects
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.amount == other.amount and self.currency == other.currency

a = Money(10, 'USD')
b = Money(10, 'USD')
c = Money(20, 'USD')

print(a == b)
print(a == c)
Note

Return NotImplemented, not False

When the other object is a different type, return NotImplemented (not False). This tells Python to let the other object try the comparison. Returning False would silently give wrong answers in some edge cases.

#Operator Overloading: __add__

You can make the + operator work with your objects by defining __add__. This is called operator overloading — you're giving an existing operator new meaning for your class.

For example, adding two Money objects should produce a new Money object with the combined amount.

__add__ makes the + operator work between two Money objects
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __add__(self, other):
        if self.currency != other.currency:
            raise ValueError('Cannot add different currencies')
        return Money(self.amount + other.amount, self.currency)

paycheck = Money(1000, 'USD')
bonus   = Money(250,  'USD')
total   = paycheck + bonus
print(total)
Common mistake

Always return a NEW object from __add__

Never modify self inside __add__. The + operator should produce a new value, just like 1 + 2 doesn't change the number 1. If you mutate self, you'll get very confusing bugs where an object's value changes unexpectedly.

#A Full Example: the Vector Class

Here's a small Vector class that puts it all together. A vector has an x and y component. We implement __repr__, __add__, __eq__, and __len__ (treating len as the number of dimensions).

A complete class with multiple dunder methods working together
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

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

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

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __len__(self):
        return 2  # always 2D

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)
print(v1 == Vector(1, 2))
print(len(v1))
Watch out

Don't overuse operator overloading

Just because you can define __add__ doesn't mean you always should. Only overload operators when the meaning is obvious and natural. Adding two Money objects makes sense. Adding two Person objects — what would that even mean? When in doubt, write a regular method with a clear name instead.

#Quick Reference: Common Dunder Methods

Here are the most useful dunder methods to know:

  • __init__(self, ...) — called when the object is created
  • __str__(self) — called by print() and str()
  • __repr__(self) — called by repr() and the console
  • __len__(self) — called by len()
  • __eq__(self, other) — called by ==
  • __add__(self, other) — called by +
  • __lt__(self, other) — called by < (less than)
  • __contains__(self, item) — called by in keyword
  • __getitem__(self, key) — called by obj[key]
Quick check

You define a class `Box` and write a `__str__` method that returns `'A box'`. What happens when you call `print(Box())`?

Key takeaways

  • Dunder methods (like __str__, __add__) are special methods Python calls automatically when you use built-in operations on your objects.
  • Define __repr__ on every class — it makes debugging much easier and acts as a fallback for __str__.
  • __str__ controls print() output (user-friendly); __repr__ is for developers and should show how to recreate the object.
  • Operator overloading (__add__, __eq__, etc.) lets your objects work with +, ==, and other operators naturally.
  • Always return a new object from __add__ — never mutate self, and return NotImplemented (not False) when types don't match in __eq__.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

What does this code print?

predict-output
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __repr__(self):
        return f"Money({self.amount!r}, {self.currency!r})"

wallet = Money(50, 'USD')
print(str(wallet))
print(repr(wallet))
Fix the bug#2

This code should print 'True' when two Vector objects have the same x and y values, but it always prints 'False'. What is wrong?

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

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

v1 = Vector(3, 5)
v2 = Vector(3, 5)
print(v1 == v2)
Fill in the blank#3

Complete the ShoppingCart class so that len(cart) returns the number of items in the cart.

class ShoppingCart:
    def __init__(self):
        self.items = []

    def add(self, item):
        self.items.append(item)

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

cart = ShoppingCart()
cart.add('apple')
cart.add('bread')
print(len(cart))
Reorder the lines#4

Put these lines in the right order to define a Point class whose + operator combines two points into a new one, then print the result.

1
    def __repr__(self):
2
        self.y = y
3
        return Point(self.x + other.x, self.y + other.y)
4
    def __add__(self, other):
5
p = Point(1, 2) + Point(3, 4)
6
print(p)
7
        self.x = x
8
        return f"Point({self.x}, {self.y})"
9
    def __init__(self, x, y):
10
class Point:
Your turn
Practice exercise

Create a Temperature class that stores a value in Celsius. Implement: 1. __init__ to accept a numeric celsius value 2. __str__ to display it nicely, e.g. '23.0°C' 3. __repr__ to show Temperature(23.0) 4. __add__ so you can add two Temperatures together (add their Celsius values) 5. __eq__ so two Temperatures are equal when their Celsius values match

Test it by creating two Temperature objects, adding them, and comparing them with ==.

Try it live — edit the code and hit Run to execute real Python:

solution.py · editable