OOP — Cheat Sheet
Python A–Z · 6 topics. Download the PDF or the Instagram carousel and share it.
Classes, __init__ & self
Classes bundle data and behaviour: __init__ initializes each instance, self is the explicit instance reference, and attributes live per-instance or shared on the class.
- ✓__init__ initializes an already-created object (creation is __new__, rarely touched)
- ✓self is explicit — obj.method() is Class.method(obj)
- ✓Class attributes are shared; assigning via self shadows them; mutable class attributes are a bug magnet
- ✓@classmethod for alternate constructors (cls), @staticmethod for namespaced utilities
class BankAccount:
bank_name = "AiCan Bank" # CLASS attribute — shared
def __init__(self, owner, balance=0):
self.owner = owner # INSTANCE attributes
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self.balance += amount
return self.balance
def __repr__(self): # developer-friendly display
return f"BankAccount({self.owner!r}, {self.balance})"
acc = BankAccount("Asha", 1000)
acc.deposit(500)
print(acc) # BankAccount('Asha', 1500)
print(acc.bank_name) # AiCan Bank — found on the class
print(BankAccount.bank_name) # same
# obj.method(args) is sugar for Class.method(obj, args)
BankAccount.deposit(acc, 100) # works — self passed explicitlyInheritance, super() & the MRO
Subclasses extend or override parent behaviour; super() delegates up the Method Resolution Order — a C3-linearized chain that makes even multiple inheritance deterministic.
- ✓super().__init__() must be called explicitly — Python will not do it for you
- ✓MRO = deterministic lookup order (C3); inspect with Class.__mro__
- ✓super() follows the MRO, not literally "the parent" — that is what makes mixins work
- ✓Prefer shallow hierarchies + composition; mixins for orthogonal features
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def payslip(self):
return f"{self.name}: {self.salary}"
class Engineer(Employee):
def __init__(self, name, salary, stack):
super().__init__(name, salary) # REQUIRED — not automatic!
self.stack = stack
def payslip(self): # extend, not replace
return super().payslip() + f" [{self.stack}]"
e = Engineer("Asha", 12_00_000, "Python")
print(e.payslip()) # Asha: 1200000 [Python]
print(isinstance(e, Employee)) # True
print(issubclass(Engineer, Employee)) # TrueDunder Methods — __str__, __eq__, __len__ & Operator Overloading
Double-underscore methods let your objects speak Python: print() calls __str__, == calls __eq__, len() calls __len__, and + calls __add__ — protocols, not magic.
- ✓__repr__ for developers (always define it); __str__ for users; containers show repr
- ✓Defining __eq__ without __hash__ makes objects unhashable — define both, consistently
- ✓__len__ + __getitem__ + __contains__ = your object works with len(), [], in, and for-loops
- ✓Return NotImplemented (not raise) from comparison dunders for foreign types
class Money:
def __init__(self, amount, currency="INR"):
self.amount = amount
self.currency = currency
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
def __str__(self):
return f"₹{self.amount:,}" if self.currency == "INR" else f"{self.amount} {self.currency}"
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return (self.amount, self.currency) == (other.amount, other.currency)
def __hash__(self):
return hash((self.amount, self.currency))
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("currency mismatch")
return Money(self.amount + other.amount, self.currency)
a, b = Money(500), Money(500)
print(a) # ₹500 (str)
print([a]) # [Money(500, 'INR')] — lists use repr!
print(a == b) # True — value equality
print(a + Money(250)) # ₹750 — our __add__
print(len({a, b})) # 1 — hashing consistent with equalityEncapsulation — @property, Underscore Conventions & Name Mangling
Python replaces getters/setters with @property — attribute syntax with method control — and replaces private keywords with conventions: _protected and __mangled.
- ✓No private keyword — _name is convention, __name is name-mangled to _Class__name
- ✓@property lets you start with plain attributes and add logic later without breaking callers
- ✓Property without a setter = read-only attribute
- ✓Never write Java-style get_x()/set_x() in Python — reviewers will flag it
class Account:
def __init__(self, balance):
self._balance = balance # internal storage
@property
def balance(self): # read: acc.balance
return self._balance
@balance.setter
def balance(self, value): # write: acc.balance = x
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
@property
def balance_lakh(self): # computed, read-only
return round(self._balance / 1_00_000, 2)
acc = Account(2_50_000)
print(acc.balance) # 250000 — attribute syntax, method behind it
acc.balance = 3_00_000 # setter validates
print(acc.balance_lakh) # 3.0
# acc.balance = -5 # ValueError
# acc.balance_lakh = 9 # AttributeError — no setterDataclasses — Boilerplate-Free Data Objects
@dataclass auto-generates __init__, __repr__ and __eq__ from type-annotated fields — with defaults, immutability (frozen=True), ordering, and field factories for mutables.
- ✓@dataclass generates __init__, __repr__, __eq__ from annotated fields
- ✓Mutable defaults MUST use field(default_factory=list) — enforced at class creation
- ✓frozen=True → immutable + hashable; order=True → sortable by field order
- ✓__post_init__ is the validation hook; asdict()/astuple() for serialization
from dataclasses import dataclass, field
@dataclass
class Student:
name: str
roll: int
cgpa: float = 0.0
skills: list[str] = field(default_factory=list) # SAFE mutable default
def __post_init__(self): # validation hook
if not 0 <= self.cgpa <= 10:
raise ValueError("cgpa out of range")
s1 = Student("Asha", 41, 8.7, ["python"])
s2 = Student("Asha", 41, 8.7, ["python"])
print(s1) # Student(name='Asha', roll=41, cgpa=8.7, skills=['python'])
print(s1 == s2) # True — field-wise __eq__ generated
# skills=[] as a plain default would raise:
# ValueError: mutable default <class 'list'> ... use default_factoryDuck Typing, ABCs & Protocols
Python trusts behaviour over declared types — "if it quacks, it's a duck". ABCs enforce interfaces at instantiation; Protocols (3.8+) bring static duck typing for type checkers.
- ✓Duck typing: capability over declared type — the default Python style
- ✓ABC + @abstractmethod fails at INSTANTIATION time, not first call — fail fast
- ✓Protocol = structural typing: satisfied by shape, checked statically, no inheritance
- ✓EAFP (try/except) over LBYL (hasattr checks) is idiomatic Python
from abc import ABC, abstractmethod
# Duck typing — no interface needed
class UPI:
def pay(self, amt): return f"UPI paid {amt}"
class Card:
def pay(self, amt): return f"Card paid {amt}"
def checkout(method, amount): # accepts ANYTHING with .pay()
return method.pay(amount)
print(checkout(UPI(), 499)) # works
print(checkout(Card(), 499)) # works
# ABC — enforced contract
class PaymentGateway(ABC):
@abstractmethod
def pay(self, amount): ...
@abstractmethod
def refund(self, txn_id): ...
class Razorpay(PaymentGateway):
def pay(self, amount): return f"rzp txn for {amount}"
def refund(self, txn_id): return f"refunded {txn_id}"
class Broken(PaymentGateway):
def pay(self, amount): return "..."
# refund missing!
Razorpay() # fine
# Broken() # TypeError: Can't instantiate abstract class