What you'll learn
Ever wondered how len(x), a + b, and for item in x work on built-in types — and how you can make your objects behave the same way? The answer is dunder methods ("double underscore"). They're the hooks Python calls behind every operator and built-in function.
By the end you'll be able to:
- Explain what special (dunder) methods are
- Give objects a useful
__str__and__repr__ - Overload operators like
+and* - Support comparison,
len(), indexing, andin
What dunder methods are
Dunder methods (also called magic or special methods) are named with leading and trailing double underscores, like __init__. You never call them directly — Python calls them for you when you use the matching syntax. Write a + b and Python runs a.__add__(b); call len(x) and it runs x.__len__(). Implementing them lets your classes plug into Python's syntax as first-class citizens.
__str__ & __repr__
The two most useful dunders control how your object appears as text. __repr__ is the unambiguous, developer-facing form (ideally something you could paste back into code); __str__ is the friendly, user-facing form used by print(). If you only write one, make it __repr__ — Python falls back to it:
class Money:
def __init__(self, dollars):
self.dollars = dollars
def __repr__(self): # for developers / debugging
return f"Money({self.dollars})"
def __str__(self): # for end users / print()
return f"${self.dollars:.2f}"
m = Money(19.5)
print(m) # uses __str__
print(repr(m)) # uses __repr__
print([m, m]) # a list uses __repr__ for its itemsTip
__repr__ for its elements — which is why a good __repr__ makes debugging so much nicer. Always give your classes at least a __repr__; the default <Money object at 0x...> tells you nothing.Operator overloading
You can define what the arithmetic operators mean for your type. __add__ powers +, __sub__ powers -, __mul__ powers *, and so on. This is how libraries like NumPy let you write a + b on whole arrays:
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other): # defines the + operator
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, k): # v * scalar
return Vector(self.x * k, self.y * k)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
print(Vector(1, 2) + Vector(3, 4)) # Vector(4, 6)
print(Vector(1, 2) * 3) # Vector(3, 6)Watch out
+ for adding vectors is intuitive; + that secretly sends an email is a trap. If a reader can't guess what the operator does, use a named method instead.Comparison methods
Define __eq__ for == and __lt__ for <, and your objects become comparable and sortable. The @total_ordering decorator is a lovely shortcut: give it __eq__ and __lt__, and it generates the rest (<=, >, >=) for you:
from functools import total_ordering
@total_ordering # fills in <=, >, >= from __eq__ and __lt__
class Temperature:
def __init__(self, degrees):
self.degrees = degrees
def __eq__(self, other):
return self.degrees == other.degrees
def __lt__(self, other):
return self.degrees < other.degrees
temps = [Temperature(20), Temperature(5), Temperature(15)]
print(sorted(t.degrees for t in temps)) # sorting now works
print(Temperature(5) < Temperature(15))Container protocols
Make your object act like a collection by implementing the container dunders: __len__ for len(), __getitem__ for indexing (which also makes it iterable!), and __contains__ for the in operator:
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self): # len(playlist)
return len(self.songs)
def __getitem__(self, i): # playlist[i] AND iteration
return self.songs[i]
def __contains__(self, song): # song in playlist
return song in self.songs
p = Playlist(["Intro", "Verse", "Chorus"])
print(len(p)) # 3
print(p[1]) # Verse
print("Chorus" in p) # True
for song in p: # __getitem__ makes it iterable
print(song)Key idea
sorted(), for loops, in, and more.Recap & quick check
Key takeaways
- Dunder (special) methods like __add__ and __len__ hook into Python's operators and built-ins.
- __repr__ is the unambiguous developer form; __str__ is the friendly form for print(). Always define __repr__.
- Overload operators (__add__, __mul__, …) — but only when the meaning is obvious.
- __eq__ + __lt__ (with @total_ordering) make objects comparable and sortable.
- __len__, __getitem__, and __contains__ make an object behave like a container.
Quick check
1. What runs when you write a + b for custom objects?
2. Which method should you always define for good debugging output?
3. What does @total_ordering do?
4. Implementing __getitem__ also gives you…
5. When should you overload an operator?
You can now make objects that feel built-in. Next up: Module 17 — Dataclasses, Enums & Structured Data.