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
— step through the idea, then dive into the details below.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.
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.
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.
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.
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))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.
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.
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)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.
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)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).
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))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 byprint()andstr()__repr__(self)— called byrepr()and the console__len__(self)— called bylen()__eq__(self, other)— called by==__add__(self, other)— called by+__lt__(self, other)— called by<(less than)__contains__(self, item)— called byinkeyword__getitem__(self, key)— called byobj[key]
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__.
What does this code print?
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))This code should print 'True' when two Vector objects have the same x and y values, but it always prints 'False'. What is wrong?
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)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))
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.
def __repr__(self):
self.y = y
return Point(self.x + other.x, self.y + other.y)
def __add__(self, other):
p = Point(1, 2) + Point(3, 4)
print(p)
self.x = x
return f"Point({self.x}, {self.y})"def __init__(self, x, y):
class Point:
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: