Cheat SheetsPython A–ZOOP

OOP — Cheat Sheet

Python A–Z · 6 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
OOP
Python A–Z6 topicsQuick revision reference
1

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
Instance vs class attributes; self demystified
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 explicitly
2

Inheritance, 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
Override + extend with super()
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))  # True
3

Dunder 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
The value-object quartet: repr, str, eq, hash
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 equality
4

Encapsulation — @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
Attribute syntax outside, control inside
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 setter
5

Dataclasses — 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
@dataclass: init + repr + eq generated
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_factory
6

Duck 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
Trust the quack — or enforce it with ABC
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
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/python