Tech With Galvan
Courses/Python Mastery/Object-Oriented Python

Dunder Methods (__str__, __len__, __eq__)

20 min • Beginner • Module 9

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

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

  1. What does print(obj) show without __str__? (The useless memory-address line)
  2. What must __str__ return? (A string)
  3. What makes len(obj) work on your class? (Defining len)
  4. Default == on your objects compares what? (Memory addresses — define eq for content)

Next: dataclasses — the same power, three lines of code.