Dunder Methods — __str__, __eq__, __len__ & Operator Overloading
IntermediateDouble-underscore methods let your objects speak Python: print() calls __str__, == calls __eq__, len() calls __len__, and + calls __add__ — protocols, not magic.
Overview
Every Python operator and built-in function is a protocol: syntax on the outside, a dunder ("double underscore") method call underneath. print(obj) → __str__, obj == other → __eq__, len(obj) → __len__, obj[i] → __getitem__, for x in obj → __iter__. Implement the right dunders and your class works with the entire language — sorting, printing, containers, loops. This is Python's answer to Java's toString/equals/hashCode/Comparable, unified into one consistent system.
repr, str & Equality
__repr__ targets developers (debugger, lists); __str__ targets users (print). Define at least __repr__. Implementing __eq__ makes == compare by value — but sets __hash__ to None unless you define it too (equal objects must hash equal).
class Money:
def __init__(self, amount, currency="INR"):
self.amount = amount
self.currency = currency
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
def __str__(self):
return f"₹{self.amount:,}" if self.currency == "INR" else f"{self.amount} {self.currency}"
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return (self.amount, self.currency) == (other.amount, other.currency)
def __hash__(self):
return hash((self.amount, self.currency))
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("currency mismatch")
return Money(self.amount + other.amount, self.currency)
a, b = Money(500), Money(500)
print(a) # ₹500 (str)
print([a]) # [Money(500, 'INR')] — lists use repr!
print(a == b) # True — value equality
print(a + Money(250)) # ₹750 — our __add__
print(len({a, b})) # 1 — hashing consistent with equalityContainer & Comparison Protocols
Implement __len__/__getitem__/__contains__ and your object behaves like a collection. __lt__ makes objects sortable without a key function.
class Playlist:
def __init__(self, *songs):
self._songs = list(songs)
def __len__(self):
return len(self._songs)
def __getitem__(self, index): # enables [] AND iteration!
return self._songs[index]
def __contains__(self, song):
return song in self._songs
pl = Playlist("Kesariya", "Tum Hi Ho", "Agar Tum Saath Ho")
print(len(pl)) # 3
print(pl[0]) # Kesariya
print("Tum Hi Ho" in pl) # True
for s in pl: # __getitem__ powers the loop
print("-", s)
class Version:
def __init__(self, major, minor):
self.t = (major, minor)
def __lt__(self, other): # sortable
return self.t < other.t
def __repr__(self):
return f"v{self.t[0]}.{self.t[1]}"
print(sorted([Version(2, 1), Version(1, 9)])) # [v1.9, v2.1]Key Points to Remember
- 1__repr__ for developers (always define it); __str__ for users; containers show repr
- 2Defining __eq__ without __hash__ makes objects unhashable — define both, consistently
- 3__len__ + __getitem__ + __contains__ = your object works with len(), [], in, and for-loops
- 4Return NotImplemented (not raise) from comparison dunders for foreign types
Interview Questions
Sign in to ask Aria__str__ vs __repr__ — when is each called, and which should you always define?
You defined __eq__ and your objects stopped working in a set. Why?
How does the in operator decide membership for a custom class?
Ask Aria about Dunder Methods — __str__, __eq__, __len__ & Operator Overloading
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.