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> ← uselessAdd __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 bothDefine __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 comparisonDefine __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 beNow 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
if not isinstance(other, Student): return NotImplemented._str_, __str_, __Str__ silently do nothing. Double underscores, both sides, exact spelling.✅ Checkpoint
__str__? *(The useless memory-address line)*__str__ return? *(A string)*len(obj) work on your class? *(Defining __len__)*== on your objects compares what? *(Memory addresses — define __eq__ for content)*Next: dataclasses — the same power, three lines of code.