Python Notes
Master Python
🧠 Real-World Analogy — Superpowers
Think of a superhero suit — Tony Stark's Iron Man armor. The suit itself is an object. When Tony raises both hands, repulsors fire. When he raises one hand, a laser fires. Each specific gesture triggers a different ability. These special gestures are like Python's magic methods — special methods that automatically trigger when you use specific Python syntax or built-in functions. They start and end with double underscores (dunder = double underscore): __method__1️⃣ __init__ — Constructor / Initializer
class Book:
def __init__(self, title, author, pages, price):
"""Called automatically when Book(...) is created."""
self.title = title
self.author = author
self.pages = pages
self.price = price
self._id = id(self) # unique Python object id
# We'll add more dunders below...Already covered in detail in constructors.mdx. Here we focus on how it interacts with other magic methods.2️⃣ __str__ and __repr__
Output:
Rule of thumb:__repr__should return a string that, when passed toeval(), recreates the object.__str__is for end users.
3️⃣ __len__ — Length
Output:
| Empty | False |
| Len | 0 |
| Len | 3 |
| Empty | True |
4️⃣ __add__, __sub__, __mul__ — Arithmetic
class Fraction:
"""Rational number with full arithmetic support."""
from math import gcd as _gcd
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ZeroDivisionError("Denominator cannot be zero.")
from math import gcd
common = gcd(abs(numerator), abs(denominator))
sign = -1 if denominator < 0 else 1
self.num = sign * numerator // common
self.den = sign * denominator // common
def __add__(self, other): # self + other
if isinstance(other, int):
other = Fraction(other)
return Fraction(self.num * other.den + other.num * self.den,
self.den * other.den)
def __radd__(self, other): # other + self (e.g., 1 + Fraction)
return self.__add__(other)
def __sub__(self, other): # self - other
if isinstance(other, int):
other = Fraction(other)
return Fraction(self.num * other.den - other.num * self.den,
self.den * other.den)
def __mul__(self, other): # self * other
if isinstance(other, int):
other = Fraction(other)
return Fraction(self.num * other.num, self.den * other.den)
def __truediv__(self, other): # self / other
if isinstance(other, int):
other = Fraction(other)
return Fraction(self.num * other.den, self.den * other.num)
def __neg__(self): # -self
return Fraction(-self.num, self.den)
def __abs__(self): # abs(self)
return Fraction(abs(self.num), self.den)
def __float__(self): # float(self)
return self.num / self.den
def __str__(self):
if self.den == 1: return str(self.num)
return f"{self.num}/{self.den}"
def __repr__(self):
return f"Fraction({self.num}, {self.den})"
a = Fraction(1, 2) # 1/2
b = Fraction(1, 3) # 1/3
c = Fraction(3, 4) # 3/4
print(f"a = {a}")
print(f"b = {b}")
print(f"a + b = {a + b}") # 1/2 + 1/3 = 5/6
print(f"a - b = {a - b}") # 1/2 - 1/3 = 1/6
print(f"a * b = {a * b}") # 1/2 * 1/3 = 1/6
print(f"a / b = {a / b}") # 1/2 / 1/3 = 3/2
print(f"a + 1 = {a + 1}") # 1/2 + 1 = 3/2
print(f"1 + a = {1 + a}") # uses __radd__
print(f"-a = {-a}")
print(f"abs(-a) = {abs(-a)}")
print(f"float(a) = {float(a)}")
print(f"a + b + c = {a + b + c}") # chained additionOutput:
5️⃣ __eq__, __lt__ — Comparison (Rich Comparison)
Output:
| Arjun (GPA | 8.90) |
| Priya (GPA | 9.40) |
| Rahul (GPA | 7.50) |
| Sneha (GPA | 9.40) |
| Vikas (GPA | 8.10) |
| Sorted (high | low GPA): |
| Priya (GPA | 9.40) |
| Sneha (GPA | 9.40) |
| Arjun (GPA | 8.90) |
| Vikas (GPA | 8.10) |
| Rahul (GPA | 7.50) |
| Top student | Priya (GPA: 9.40) |
| Arjun (GPA | 8.90) == Priya (GPA: 9.40): False |
| Arjun (GPA | 8.90) < Priya (GPA: 9.40): True |
| Arjun (GPA | 8.90) > Priya (GPA: 9.40): False |
| Arjun (GPA | 8.90) <= Priya (GPA: 9.40): True |
| Arjun (GPA | 8.90) >= Priya (GPA: 9.40): False |
| Top students (GPA ≥ 9.0) | {Student('Priya', 9.4), Student('Sneha', 9.4)} |
6️⃣ __getitem__, __setitem__, __delitem__ — Container Protocol
Output:
7️⃣ __iter__ and __next__ — Iterator Protocol
Output:
| Even numbers | [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] |
| 10 in evens | True |
| 7 in evens | False |
| len(evens) | 10 |
| Countdown | 5 4 3 2 1 🚀 |
| Squares | [1, 4, 9, 16, 25] |
8️⃣ __call__ — Make Objects Callable
9️⃣ __enter__ and __exit__ — Context Manager
class DatabaseTransaction:
"""Context manager for database transactions."""
def __init__(self, db_name):
self.db_name = db_name
self._operations = []
self._committed = False
def __enter__(self):
print(f"🔓 BEGIN TRANSACTION on '{self.db_name}'")
return self # returned as 'as' variable
def execute(self, sql):
self._operations.append(sql)
print(f" 📝 Queued: {sql}")
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
# No exception — commit
print(f" ✅ COMMIT: {len(self._operations)} operations committed.")
self._committed = True
else:
# Exception occurred — rollback
print(f" ❌ ROLLBACK: {exc_type.__name__}: {exc_val}")
self._operations.clear()
print(f"🔒 END TRANSACTION on '{self.db_name}'")
return True # suppress exception (return False to propagate)
# Successful transaction
with DatabaseTransaction("users_db") as tx:
tx.execute("INSERT INTO users (name) VALUES ('Arjun')")
tx.execute("UPDATE balance SET amount = 5000 WHERE user_id = 1")
print()
# Failed transaction — auto rollback
with DatabaseTransaction("orders_db") as tx:
tx.execute("INSERT INTO orders VALUES (101, 'Laptop')")
raise ValueError("Payment gateway timeout!")
tx.execute("UPDATE inventory SET qty = qty - 1") # never reached
print("\nProgram continues after handled exception ✅")Output:
| 📝 Queued | INSERT INTO users (name) VALUES ('Arjun') |
| 📝 Queued | UPDATE balance SET amount = 5000 WHERE user_id = 1 |
| ✅ COMMIT | 2 operations committed. |
| 📝 Queued | INSERT INTO orders VALUES (101, 'Laptop') |
| ❌ ROLLBACK | ValueError: Payment gateway timeout! |
🏆 Complete Example — ShoppingCart
Output:
| 'USB-C Hub' in cart | True |
| cart[0] | Python Mastery Book ₹ 599.00 × 2 = ₹ 1198.00 |
| Most expensive | Mechanical Keyboard ₹ 3499.00 × 1 = ₹ 3499.00 |
🇮🇳 Hindi Explanation (हिंदी में समझें)
Magic methods kya hain? Magic methods (dunder methods) are special methods that Python calls automatically when you perform specific operations — like using+, callinglen(), or running aforloop. They let you define how your objects behave with Python's built-in syntax.
What is the difference between__str__and__repr__?__str__provides user-readable output (what shows up in print).__repr__provides developer-oriented technical output — ideally a string that can recreate the object viaeval().
Why do we use__iter__and__next__? By defining these, you can use your class objects inforloops,list(),sum()and everywhere else.__iter__returns the iterator,__next__yields one value at a time, and raisesStopIterationat the end.
@total_orderingkya karta hai? If you only define__eq__and__lt__,@total_orderingautomatically generates<=,>,>=for you. Very convenient!
❓ Interview Questions
Q1: What are dunder/magic methods? Give 5 examples.
Answer: Magic methods are special methods with double underscores (__method__) that Python calls automatically for specific operations:
__init__: object construction__str__: string representation (print())__len__: length (len())__add__: addition (+)__iter__: iteration (forloop)
Q2: What is the difference between __str__ and __repr__?
Answer:
__str__: User-friendly string — used byprint(),str(). Should be readable.__repr__: Developer-facing, unambiguous string — used byrepr(), REPL, list display. Should ideally recreate the object viaeval().
If __str__ is not defined, Python falls back to __repr__.
Q3: When do you need to define __hash__ along with __eq__?
Answer: When you define __eq__, Python automatically sets __hash__ = None, making objects unhashable (can't be used in sets or as dict keys). To allow hashing, explicitly define __hash__. A good rule: hash() should be consistent with equality: a == b ⟹ hash(a) == hash(b).
Q4: What does __call__ do? Give a use case.
Answer: __call__ makes an instance callable like a function (obj(args)). Use cases: callable objects with state (memoization, counters), function decorators as classes, strategy pattern objects.
Q5: Explain the context manager protocol (__enter__, __exit__).
Answer:
__enter__: Called whenwithblock starts. Returns the object bound toas.__exit__(exc_type, exc_val, exc_tb): Called when block exits (normally or via exception). ReturnTrueto suppress the exception;False/Noneto propagate it.
Use cases: file handling, database transactions, lock management.
Q6: What is @total_ordering?
Answer: A class decorator from functools. If you define __eq__ and one of (__lt__, __le__, __gt__, __ge__), @total_ordering automatically generates the remaining comparison methods. Saves writing 4 extra methods.
Q7: What is __contains__? How does it relate to __iter__?
Answer: __contains__ is called when you use item in obj. If not defined, Python falls back to iterating with __iter__ and checking each element. Define __contains__ for O(1) membership testing (e.g., hash-based lookup) instead of O(n) linear scan.
Q8: What happens if __iter__ returns self vs a new iterator?
Answer:
- Returns
self: The object is both iterable AND iterator. Can only be iterated once (callingforloop again restarts only if__iter__resets state). - Returns new object: Iterable produces fresh iterators each time — multiple independent loops work correctly.
Best practice for containers: __iter__ returns iter(self._data) (fresh iterator each call).
📊 Complete Magic Methods Reference
| Category | Method | Triggered by |
|---|---|---|
| Construction | __new__, __init__, __del__ | Class(), garbage collection |
| Representation | __str__, __repr__, __format__ | print(), repr(), f-strings |
| Numeric | __int__, __float__, __bool__, __abs__, __neg__ | int(), float(), bool(), abs(), unary - |
| Arithmetic | __add__, __sub__, __mul__, __truediv__, __mod__, __pow__ | +, -, *, /, %, ** |
| Comparison | __eq__, __ne__, __lt__, __le__, __gt__, __ge__ | ==, !=, <, <=, >, >= |
| Container | __len__, __getitem__, __setitem__, __delitem__, __contains__ | len(), obj[k], obj[k]=v, del obj[k], in |
| Iteration | __iter__, __next__ | for, iter(), next() |
| Context | __enter__, __exit__ | with statement |
| Callable | __call__ | obj(args) |
| Attribute | __getattr__, __setattr__, __delattr__ | obj.x, obj.x=v, del obj.x |
| Hashing | __hash__ | hash(), dict keys, sets |
🧪 Practice Exercises
- Money Class: Implement
Money(amount, currency)with__add__,__sub__,__mul__,__eq__,__lt__,__str__,__repr__. RaiseTypeErrorfor cross-currency ops. - Inventory System: Build
Inventorywith__getitem__(by name),__setitem__,__delitem__,__contains__,__iter__,__len__,__str__. - Polynomial: Create a
Polynomialclass with coefficient list. Implement__add__,__mul__,__call__(evaluate at x),__str__(format nicely like "3x² + 2x + 1"). - Lazy File Reader: Implement
LazyFileReader(filename)as an iterator —__iter__opens the file,__next__yields one line at a time, closes onStopIteration. - Rate Limiter (callable): Create a
RateLimiter(max_calls, period)callable class that allows at mostmax_callsinvocations perperiodseconds, raisingRuntimeErrorotherwise.
🎉 Congratulations! You've completed the entire OOP section of the Python Master Course. What's Next? → File I/O, Exception Handling, Decorators, Generators, and Python Standard Library!
Continue the lesson path