Dunder Methods (str, len, eq)
Print your own class object and you get <__main__.Student object at 0x7f...> — useless. Dunder methods (double-underscore methods) are hooks that plug your objects into Python’s built-ins, so print(), len(), and == understand them.
str — your object’s readable face
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
s = Student("Aarav", 92)
print(s) # <__main__.Student object at 0x7f8b2c3d4e50> ← useless
Add __str__ and transform that:
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
def __str__(self):
return f"Student({self.name}, marks={self.marks})"
s = Student("Aarav", 92)
print(s) # Student(Aarav, marks=92) ← human-readable!
__str__ must return a string — that string is what print() and str() display. Any object you’ll ever print deserves one.
repr — the developer version (one-line mention)
Python actually looks for two methods: __str__ for users, __repr__ for developers (debuggers, console). The pragmatic beginner rule:
__repr__ = __str__ # add this line inside the class — one method for both
Define __str__, alias __repr__ to it, and every context shows your nice version.
len — making len() work
class Playlist:
def __init__(self, name, songs):
self.name = name
self.songs = songs
def __len__(self):
return len(self.songs)
def __str__(self):
return f"Playlist '{self.name}' ({len(self.songs)} songs)"
p = Playlist("Focus", ["Track A", "Track B", "Track C"])
print(len(p)) # 3 ← len() now understands your object!
print(p) # Playlist 'Focus' (3 songs)
len(p) works because Python asks the object: “do you have a __len__?” If yes, it calls it. Your class just joined Python’s inner circle.
eq — controlling ==
By default, == on your objects compares memory addresses — two identical students are “not equal”:
a = Student("Aarav", 92)
b = Student("Aarav", 92)
print(a == b) # False — different objects, default comparison
Define __eq__ to compare by what matters:
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
def __eq__(self, other):
return self.name == other.name and self.marks == other.marks
def __str__(self):
return f"Student({self.name}, marks={self.marks})"
a = Student("Aarav", 92)
b = Student("Aarav", 92)
print(a == b) # True — equal by content, exactly as it should be
Now two students with the same name and marks ARE equal — the semantics your app actually needs.
The dunder pattern in one thought
Every dunder method follows the same idea:
Python has a hook for everything it does — define the hook, customize the behavior.
| You write | Python calls | Meaning |
|---|---|---|
print(obj) |
__str__ |
display form |
len(obj) |
__len__ |
“size” of the object |
obj1 == obj2 |
__eq__ |
what equality means |
obj + other |
__add__ |
custom addition (yes, even +) |
You’ll meet __add__, __getitem__ and friends as you read bigger codebases. The mechanism never changes.
Common Errors & Fixes
__str__must return a string — returning a number crashes with a TypeError. Wrap in f-string.__eq__crashes on wrong types — guard it:if not isinstance(other, Student): return NotImplemented.- Typo in dunder names —
_str_,__str_,__Str__silently do nothing. Double underscores, both sides, exact spelling.
✅ Checkpoint
- What does print(obj) show without
__str__? (The useless memory-address line) - What must
__str__return? (A string) - What makes
len(obj)work on your class? (Defining len) - Default
==on your objects compares what? (Memory addresses — define eq for content)
Next: dataclasses — the same power, three lines of code.