Python105 min total · 18 parts
Python Fundamentals for Interviews: Data Structures, Comprehensions, and Gotchas
Part 14 of 18 · ~3 min
Dunder Methods and Operator Overloading
Every time you've written Report(entries) and had __init__ run automatically, you've already been relying on a dunder method without necessarily thinking of it as one — Python reserves this double-underscore naming for the specific methods it looks for by name when built-in syntax needs to talk to an object. +, ==, len(), print() — none of these are special-cased for Report; each one is Python checking whether the object in front of it happens to define the matching dunder, and calling it if so:
class Report:
def __init__(self, entries):
self.entries = entries
def __repr__(self):
return f"Report(entries={len(self.entries)})" # unambiguous, dev-facing
def __eq__(self, other):
return self.entries == other.entries
def __add__(self, other):
return Report(self.entries + other.entries) # merge two batches into one report
def __len__(self):
return len(self.entries)
morning = Report(morning_entries)
afternoon = Report(afternoon_entries)
daily = morning + afternoon # calls morning.__add__(afternoon)
len(daily) # total entries across both batches
print(daily) # Report(entries=1842) — calls __repr__
| Syntax | Dunder method called |
|---|---|
obj + other | __add__ |
obj == other | __eq__ |
obj[key] | __getitem__ |
len(obj) | __len__ |
str(obj) / print(obj) | __str__ (falls back to __repr__) |
for x in obj | __iter__ |
with obj: | __enter__ / __exit__ |
__repr__ vs. __str__
These two exist for different readers, and mixing them up costs you exactly one of the two. __str__ is written for whoever's looking at the output of the program itself — a report emailed to an ops engineer, say — so it can afford to drop detail in favor of being pleasant to read. __repr__ is written for whoever's looking at the code — it shows up in a REPL echoing a bare expression, inside a list or dict's own printed form, and anywhere __str__ hasn't been defined, since Python falls back to it — and the convention is to make it precise enough that, ideally, pasting it back into Python would reconstruct the object. Skip __str__ entirely and print(daily) still does something reasonable, because it borrows __repr__. Skip __repr__ and define only __str__, and the moment a Report shows up inside a list or a stack trace, you're staring at Python's own generic fallback, <Report object at 0x7f...>, which tells you nothing.
One thing worth knowing before you add __eq__ to a class of your own: it quietly turns off hashing. Python's reasoning is that if two objects can be equal, they'd better hash the same way, and since it has no way to guarantee that from an __eq__ it didn't write, it sets __hash__ to None the instant you define __eq__ yourself:
{morning, afternoon} # TypeError: unhashable type: 'Report'
That's not a bug — a Report genuinely shouldn't be a set member or a dict key if "equal" means "currently has the same entries," since entries can change underneath it. But it catches people who add __eq__ for one reason and then can't figure out why some unrelated part of the codebase, three files away, suddenly can't put instances of the class in a set anymore. Define __hash__ explicitly — typically based on whatever field genuinely never changes — if you need both.
One more dunder is worth adding to Report directly, because it collects something the generators chapter planted: making the class satisfy the iterator protocol itself, so for entry in report just works without anyone reaching for report.entries by name.
class Report:
def __init__(self, entries):
self.entries = entries
def __iter__(self):
return iter(self.entries) # hand off to the list's own iterator
for entry in Report(morning_entries): # no .entries anywhere in sight
print(entry.ip)
__iter__ only has to return something satisfying the protocol — it doesn't have to implement __next__ itself. Delegating to iter(self.entries) is the whole implementation, and it's the same trick a hand-written generator was doing all along, just attached to a class instead of a function.