Courses/Python Mastery/Module 9: Object-Oriented Python
Module 9 · Lesson 420 minBeginner

Dunder Methods (__str__, __len__, __eq__)

Lesson goal
Make your objects work with Python's built-ins like print and len.

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

code
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:

code
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:

code
__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

code
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":

code
a = Student("Aarav", 92)
b = Student("Aarav", 92)
print(a == b)     # False — different objects, default comparison

Define __eq__ to compare by *what matters*:

code
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 writePython callsMeaning
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.