Special (Dunder) Methods

Python · Lesson 39 of 56 6 min read Updated

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 writePython callsPurpose
repr(x), shown in lists and the REPLx.__repr__()Developer-friendly text
str(x), print(x), f-stringsx.__str__()User-friendly text
x == yx.__eq__(y)Equality
x < yx.__lt__(y)Ordering
x + yx.__add__(y)Addition
len(x)x.__len__()Size
x[i]x.__getitem__(i)Indexing
item in xx.__contains__(item)Membership
for item in xx.__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.

PythonRun it yourself
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})")
Output
₹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.

PythonRun it yourself
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)}))
Output
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.

PythonRun it yourself
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)]))
Output
₹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.

PythonRun it yourself
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", [])))
Output
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.

PythonRun it yourself
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")
Output
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) and a + b, not x.__len__() or a.__add__(b).
  • Changing self in __add__. Return a new object; a + b should never modify a.
  • Raising an error for unknown types. Return NotImplemented so Python can try other options and produce the normal error message.
  • Defining __eq__ and then using objects in a set. Without __hash__, you get TypeError: 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 + and sum(); return new objects and NotImplemented for unknown types.
  • __len__, __getitem__, __contains__ and __iter__ make a class act like a container.
  • __enter__ and __exit__ let objects work in with blocks.

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
Python
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)
Output
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?

2. Which method makes item in collection work?

3. Why add __radd__ to a Money class?

4. What should __eq__ return when compared with an unsupported type?

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