Special (Dunder) Methods
Why does len() work on strings, lists and dictionaries alike? Why can you add two numbers with + and also join two lists with +? Because each of these types defines special methods: methods with names that start and end with two underscores, such as __len__ and __add__. Programmers call them dunder methods (short for "double underscore").
You never call them directly. Python calls them for you when you use built-in syntax. By writing them in your own classes, you make your objects feel like they were built into the language.
How Python uses dunder methods
When you write len(cart), Python actually runs cart.__len__(). When you write a + b, it runs a.__add__(b). Here are the ones you will use most:
| You write | Python calls | Purpose |
|---|---|---|
repr(x), shown in lists and the REPL | x.__repr__() | Developer-friendly text |
str(x), print(x), f-strings | x.__str__() | User-friendly text |
x == y | x.__eq__(y) | Equality |
x < y | x.__lt__(y) | Ordering |
x + y | x.__add__(y) | Addition |
len(x) | x.__len__() | Size |
x[i] | x.__getitem__(i) | Indexing |
item in x | x.__contains__(item) | Membership |
for item in x | x.__iter__() | Looping |
with x: | x.__enter__(), x.__exit__() | Set-up and clean-up |
You already know one dunder method: __init__, from Classes & Objects.
__repr__ and __str__: how objects print
Without help, printing an object shows its class and memory address, which tells you nothing. __repr__ should return an unambiguous description for developers, ideally text that looks like the code to recreate the object. __str__ returns a friendly version for users. If you write only one, write __repr__: Python falls back to it when __str__ is missing.
class Money:
def __init__(self, rupees):
self.rupees = rupees
def __repr__(self):
return f"Money({self.rupees})"
def __str__(self):
return f"₹{self.rupees:,}"
price = Money(1299)
print(price)
print(repr(price))
print([Money(50), Money(1200)])
print(f"Total: {price} ({price!r})")₹1,299 Money(1299) [Money(50), Money(1200)] Total: ₹1,299 (Money(1299))
Notice that a list shows its items using __repr__, and !r inside an f-string asks for repr() instead of str().
Comparing objects: __eq__ and __lt__
By default, == on your objects checks whether they are the same object, not whether they hold the same data. Write __eq__ to compare data, and __lt__ (less than) to allow sorting. The functools.total_ordering decorator then fills in <=, > and >= from those two.
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, rupees):
self.rupees = rupees
def __repr__(self):
return f"Money({self.rupees})"
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.rupees == other.rupees
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.rupees < other.rupees
def __hash__(self):
return hash(self.rupees)
print(Money(500) == Money(500))
print(Money(500) >= Money(750))
print(sorted([Money(999), Money(49), Money(250)]))
print(len({Money(10), Money(10), Money(20)}))True False [Money(49), Money(250), Money(999)] 2
Returning the special value NotImplemented (not an error) tells Python "I do not know how to compare with that type"; Python then tries the other object or falls back to a sensible default. One more rule: if you define __eq__, Python removes the default __hash__, which makes objects unusable in sets and as dictionary keys. Define __hash__ too, based on the same data, if the objects should not change. The dataclasses module can write all of this for you.
Arithmetic: __add__ and __radd__
To support +, write __add__. It should return a new object rather than changing either side. Adding __radd__ (right add) handles the case where your object is on the right of +, which is what lets sum() work, because sum() starts with 0 + first_item.
class Money:
def __init__(self, rupees):
self.rupees = rupees
def __str__(self):
return f"₹{self.rupees:,}"
def __add__(self, other):
if isinstance(other, Money):
return Money(self.rupees + other.rupees)
if isinstance(other, int):
return Money(self.rupees + other)
return NotImplemented
__radd__ = __add__
def __mul__(self, times):
return Money(self.rupees * times)
chai, samosa = Money(20), Money(15)
print(chai + samosa)
print(chai * 4 + samosa * 2)
print(sum([Money(1299), Money(499), Money(249)]))₹35 ₹110 ₹2,047
The line __radd__ = __add__ simply reuses the same method, because adding money is the same in both directions.
Behaving like a container
A class that holds a collection of items can support len(), indexing, in and for loops. Here a playlist keeps its songs in a list and passes these operations on to it.
class Playlist:
def __init__(self, name, songs):
self.name = name
self.songs = list(songs)
def __len__(self):
return len(self.songs)
def __getitem__(self, index):
return self.songs[index]
def __contains__(self, song):
return song.lower() in (s.lower() for s in self.songs)
def __iter__(self):
return iter(self.songs)
def __bool__(self):
return len(self.songs) > 0
road_trip = Playlist("Jaipur drive", ["Kesariya", "Ilahi", "Safarnama"])
print(len(road_trip), road_trip[0], road_trip[-1])
print("ilahi" in road_trip)
for number, song in enumerate(road_trip, start=1):
print(number, song)
print(bool(Playlist("Empty", [])))3 Kesariya Safarnama True 1 Kesariya 2 Ilahi 3 Safarnama False
Our __contains__ ignores capital letters, which the plain list would not do. __bool__ decides whether the object counts as true in an if statement; without it, Python uses __len__, so an empty playlist would be false anyway. You will learn more about __iter__ in Iterators & Generators.
Context managers: __enter__ and __exit__
The with statement you used for files works with any object that has __enter__ and __exit__. Python calls __enter__ at the start of the block and __exit__ at the end, even if an error happened inside.
class ShopShift:
def __init__(self, staff):
self.staff = staff
def __enter__(self):
print(f"{self.staff} opens the shutter")
return self
def __exit__(self, exc_type, exc_value, traceback):
print(f"{self.staff} locks the shop")
return False # do not hide errors
with ShopShift("Kabir") as shift:
print("Selling chai and biscuits")Kabir opens the shutter Selling chai and biscuits Kabir locks the shop
The value returned by __enter__ is what as shift receives. If __exit__ returns False, any error in the block continues upwards after the clean-up runs.
Common mistakes
- Calling dunders directly. Write
len(x)anda + b, notx.__len__()ora.__add__(b). - Changing
selfin__add__. Return a new object;a + bshould never modifya. - Raising an error for unknown types. Return
NotImplementedso Python can try other options and produce the normal error message. - Defining
__eq__and then using objects in a set. Without__hash__, you getTypeError: unhashable type.
Key points
- Dunder methods have names like
__len__; Python calls them when you use built-in syntax. __repr__gives developer text and__str__gives user text; write__repr__first.__eq__and__lt__(plus@total_ordering) make objects comparable and sortable; pair__eq__with__hash__.__add__and__radd__support+andsum(); return new objects andNotImplementedfor unknown types.__len__,__getitem__,__contains__and__iter__make a class act like a container.__enter__and__exit__let objects work inwithblocks.
Exercise
Write a class Cart for an online store in Pune that stores item names with prices in a dictionary. Give it an add(name, price) method and these dunder methods: __len__ (number of items), __contains__ (is an item name in the cart), __iter__ (loop over item names) and __str__ (a line such as Cart: 3 items, ₹2,047). Add three items, then test len(), in, a for loop and print().
Show solution
class Cart:
def __init__(self):
self.items = {}
def add(self, name, price):
self.items[name] = price
def __len__(self):
return len(self.items)
def __contains__(self, name):
return name in self.items
def __iter__(self):
return iter(self.items)
def __str__(self):
return f"Cart: {len(self)} items, ₹{sum(self.items.values()):,}"
cart = Cart()
cart.add("Earphones", 1299)
cart.add("Phone cover", 499)
cart.add("Charging cable", 249)
print(len(cart), "Phone cover" in cart, "Laptop" in cart)
for name in cart:
print("-", name)
print(cart)3 True False - Earphones - Phone cover - Charging cable Cart: 3 items, ₹2,047
Each dunder method passes the work on to the dictionary, so the cart supports the same built-in syntax with very little code.
Quick Quiz
1. What does print(obj) call to get the text?
print() uses str(), which calls __str__ or falls back to __repr__.
2. Which method makes item in collection work?
Membership tests call __contains__ (Python can fall back to iterating if it is missing).
3. Why add __radd__ to a Money class?
0 + Money(...) fails on int, so Python tries Money.__radd__.
4. What should __eq__ return when compared with an unsupported type?
NotImplemented lets Python try the other object and fall back to default behaviour.
Learn Python with a mentor
Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
