Excellent.
DAY 21 is where Python’s elegance comes from. Dunder (magic) methods are protocols, not tricks. Master them, and Python becomes predictable, extensible, and interview-friendly.
🧠 DAY 21 — Dunder Methods (Magic Methods & Python Protocols)
🔑 CORE IDEA OF DAY 21
Dunder methods are how Python asks objects to behave.
Operators, printing, iteration, context managers — all are method calls under the hood.
1️⃣ What Are Dunder Methods?
Dunder = double underscore methods like __add__, __str__, __len__.
They are hooks Python calls automatically when you:
- Use operators (
+,==) - Print objects
- Iterate
- Enter
withblocks - Access attributes
They define protocols (behavior contracts).
2️⃣ Operators Are Just Method Calls
a + b
Is really:
a.__add__(b)
If that fails, Python may try:
b.__radd__(a)
Same idea for:
==→__eq__<→__lt__[]→__getitem__len()→__len__
🧠 Syntax is sugar; methods do the work.
3️⃣ String Representation: __str__ vs __repr__ (INTERVIEW FAVORITE)
class User:
def __init__(self, name):
self.name = name
__str__ → user-friendly
def __str__(self):
return f"User({self.name})"
__repr__ → developer/debug-friendly
def __repr__(self):
return f"User(name={self.name!r})"
Rules:
print(obj)→__str__- Interactive shell →
__repr__ - If
__str__missing → falls back to__repr__
Best practice:
Make
__repr__unambiguous and, if possible, evaluable.
4️⃣ Truthiness Protocol: __bool__ & __len__
When Python evaluates:
if obj:
Order:
- Call
obj.__bool__()if exists - Else call
obj.__len__() - Else →
True
Example:
class Box:
def __len__(self):
return 0
bool(Box()) # False
This explains truthy/falsy behavior precisely.
5️⃣ Container Protocols: __len__, __getitem__, __contains__
class Bag:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
def __getitem__(self, i):
return self.items[i]
Now:
b = Bag([1, 2, 3])
len(b) # works
b[0] # works
for x in b: # works (iteration via __getitem__)
🧠 Implementing a few methods unlocks many behaviors.
6️⃣ Iteration Protocol (Clean Way)
Preferred iteration uses:
__iter____next__
class Counter:
def __init__(self, n):
self.n = n
def __iter__(self):
self.i = 0
return self
def __next__(self):
if self.i >= self.n:
raise StopIteration
self.i += 1
return self.i
This is how for-loops really work.
7️⃣ Attribute Access Hooks (Advanced but Powerful)
__getattr__ — fallback lookup
def __getattr__(self, name):
return "missing"
__getattribute__ — intercept all access (dangerous)
def __getattribute__(self, name):
...
⚠️ Misuse can cause infinite recursion.
Use only when necessary (ORMs, proxies).
8️⃣ Context Managers: with Statement (VERY IMPORTANT)
with open("file.txt") as f:
data = f.read()
Under the hood:
f = open(...)
f.__enter__()
try:
...
finally:
f.__exit__(...)
Custom context manager:
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc, tb):
import time
print(time.time() - self.start)
This guarantees cleanup, even on exceptions.
9️⃣ Arithmetic & Comparison Protocols
Common ones:
__add__,__sub__,__mul____eq__,__lt__,__le____hash__(immutability matters!)
Example:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
Now Point can be a dict key.
🔟 Object Lifecycle Hooks
__new__→ object creation (Day 19)__init__→ initialization__del__→ finalizer (⚠️ unreliable; avoid)
Rule:
Use context managers instead of
__del__.
11️⃣ Protocol Thinking (Big Mental Shift)
Python prefers protocols over inheritance.
If an object:
- Has
__len__→ it’s “sized” - Has
__iter__→ it’s “iterable” - Has
__enter__/__exit__→ it’s a context manager
No base class required.
🧠 Duck typing in action.
🔥 INTERVIEW TRAPS (DAY 21)
Q1
class A:
def __len__(self):
return 0
print(bool(A()))
✔ False
Q2
Why __repr__ is preferred for debugging?
✔ Unambiguous
✔ Shows internal state
✔ Used by interactive shell
Q3
What happens if both __bool__ and __len__ exist?
✔ __bool__ wins
🧠 DAY 21 MENTAL CHECKLIST
When designing a class:
- What protocols should it support?
- Do I need iteration?
- How should it print?
- Should it be hashable?
- Can a context manager simplify cleanup?
📝 DAY 21 ASSIGNMENT (MANDATORY)
1️⃣ Predict output (NO RUNNING):
class A:
def __str__(self):
return "A"
def __repr__(self):
return "REP"
a = A()
print(a)
print([a])
2️⃣ Explain clearly:
- Difference between
__str__and__repr__ - Why protocols beat inheritance in Python
3️⃣ Design question:
How would you design a resource-safe database connection object?
(Hint: context manager + composition)
🔜 DAY 22 PREVIEW
DAY 22 — Exceptions Internals (Hierarchy, Cost & Best Practices)
You’ll learn:
- Exception hierarchy
- Why exceptions are slow
- How
try/exceptreally works - Writing clean, Pythonic error handling
When ready, say 👉 “START DAY 22”