Python OOP (Object-Oriented Programming) — The Complete Notebook
Master Object-Oriented Programming in Python: class & instance lifecycle (__new__ vs __init__), 4 pillars, MRO & super(), dunder methods, __slots__, descriptors, dataclasses, Protocols, and SOLID design patterns.
Python Object-Oriented Programming (OOP)
1. Overview & Core Philosophy#
Python is a multi-paradigm language, but its object model is foundational: everything in Python is an object — including functions, modules, lists, and even primitive types like int and str.
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects, which contain data (in the form of attributes or fields) and code (in the form of methods or procedures).
mermaidgraph TD OOP["Python OOP Core Pillars"] OOP --> Enc["1. Encapsulation<br/>(Data Hiding & Properties)"] OOP --> Abs["2. Abstraction<br/>(ABCs & Interfaces)"] OOP --> Inh["3. Inheritance<br/>(Code Reuse & Hierarchy)"] OOP --> Poly["4. Polymorphism<br/>(Duck Typing & Dynamic Dispatch)"]
In Python, functions are first-class objects (instances of
function), classes are objects (instances oftype), and numbers are objects (instances ofint/float).
2. Classes, Objects & The Instance Lifecycle#
2.1 Class Definition & The Role of self#
A class is a blueprint for creating objects. An instance is a concrete object created from that blueprint.
self represents the specific instance of the class upon which a method is called. Python passes this instance automatically as the first argument when calling instance methods.
🐍 PythonInteractive WebAssemblyclass BankAccount:
"""Blueprint for a standard bank account."""
def __init__(self, account_holder: str, balance: float = 0.0) -> None:
# Instance attributes (unique to each instance)
self.account_holder = account_holder
self.balance = balance
def deposit(self, amount: float) -> float:
if amount <= 0:
raise ValueError("Deposit amount must be positive.")
self.balance += amount
return self.balance
def withdraw(self, amount: float) -> float:
if amount > self.balance:
raise ValueError("Insufficient funds.")
self.balance -= amount
return self.balance
# Instantiation
acc1 = BankAccount("Alice", 1000.0)
acc2 = BankAccount("Bob", 250.0)
acc1.deposit(500.0)
print(acc1.balance) # 1500.0
print(acc2.balance) # 250.0 (independent state)
# Under the hood: acc1.deposit(500) is equivalent to:
BankAccount.deposit(acc1, 500.0)
2.2 Object Creation: __new__ vs __init__#
Many developers assume __init__ is the constructor, but in Python:
__new__(cls, *args, **kwargs)is the actual constructor / allocator (creates and returns the raw instance).__init__(self, *args, **kwargs)is the initializer (configures the freshly created instance).
🐍 PythonInteractive WebAssemblyclass ImmutableCoordinate:
"""Overriding __new__ to customize instance creation before initialization."""
def __new__(cls, x: float, y: float):
print(f"1. Allocating memory for {cls.__name__} instance")
instance = super().__new__(cls)
return instance
def __init__(self, x: float, y: float) -> None:
print("2. Initializing instance state")
self.x = x
self.y = y
point = ImmutableCoordinate(10.5, 20.0)
# Output:
# 1. Allocating memory for ImmutableCoordinate instance
# 2. Initializing instance state
Use
__new__when:
- Subclassing immutable types like
int,str, ortuple.- Implementing Creational patterns like Singletons or Object Pooling.
- Metaprogramming and custom class factories.
2.3 Class Attributes vs Instance Attributes#
- Instance attributes: Owned by a specific instance, stored in the instance's
__dict__. - Class attributes: Owned by the class itself, shared across all instances of that class.
🐍 PythonInteractive WebAssemblyclass ServerNode:
# Class attribute (shared by all nodes)
cluster_region = "us-east-1"
total_nodes = 0
def __init__(self, node_id: str, ip_address: str) -> None:
# Instance attributes (isolated per node)
self.node_id = node_id
self.ip_address = ip_address
ServerNode.total_nodes += 1
n1 = ServerNode("node-01", "10.0.0.1")
n2 = ServerNode("node-02", "10.0.0.2")
print(ServerNode.total_nodes) # 2
print(n1.cluster_region) # "us-east-1"
print(n2.cluster_region) # "us-east-1"
# CAUTION: Modifying a class attribute via an instance creates an instance shadow variable!
n1.cluster_region = "eu-central-1" # Creates n1.__dict__['cluster_region']
print(n1.cluster_region) # "eu-central-1" (instance attribute)
print(n2.cluster_region) # "us-east-1" (class attribute unaffected)
print(ServerNode.cluster_region) # "us-east-1"
The Mutable Class Attribute Trap: Never assign mutable objects (like lists or dictionaries) as class attributes unless you intentionally want all instances to mutate the exact same shared object!
🐍 PythonInteractive WebAssembly# INCORRECT: INCORRECT (Shared mutable state bug)
class UserBug:
roles = [] # Shared across every instance!
# CORRECT: CORRECT
class UserCorrect:
def __init__(self):
self.roles = [] # Unique to each instance
3. The Three Types of Methods: Instance, Class & Static#
| Method Type | Decorator | First Parameter | Can Access / Mutate | Primary Use Case |
|---|---|---|---|---|
| Instance Method | (None) | self | Instance state (self) + Class state (self.__class__) | Standard behaviors and business logic |
| Class Method | @classmethod | cls | Class state (cls), cannot access self | Factory methods, alternative constructors |
| Static Method | @staticmethod | (None) | Cannot access self or cls | Self-contained utilities bound to class namespace |
🐍 PythonInteractive WebAssemblyfrom datetime import date
from typing import Self
class Employee:
base_salary_min = 40_000
def __init__(self, name: str, salary: float, birth_year: int) -> None:
self.name = name
self.salary = max(salary, Employee.base_salary_min)
self.birth_year = birth_year
# 1. Instance Method (Operates on self)
def calculate_bonus(self, percentage: float) -> float:
return self.salary * (percentage / 100)
# 2. Class Method (Alternative Constructor / Factory)
@classmethod
def from_birth_year(cls, name: str, salary: float, birth_year: int) -> Self:
"""Factory method to construct an employee instance."""
return cls(name=name, salary=salary, birth_year=birth_year)
@classmethod
def update_minimum_salary(cls, new_min: float) -> None:
cls.base_salary_min = new_min
# 3. Static Method (Pure function scoped inside the class)
@staticmethod
def is_valid_age(birth_year: int) -> bool:
current_year = date.today().year
age = current_year - birth_year
return 18 <= age <= 70
# Usage:
emp = Employee.from_birth_year("Sarah Connor", 75_000, 1985)
print(emp.calculate_bonus(10)) # 7500.0
print(Employee.is_valid_age(1995)) # True
4. Encapsulation & The @property Decorator#
Encapsulation restricts direct access to an object's internal components, preventing accidental modification and keeping interfaces clean.
4.1 Access Modifiers (Naming Conventions)#
Python does not enforce private attributes at compile-time. Instead, it uses intentional naming conventions:
- Public (
var): Accessible everywhere. - Protected (
_var): Convention indicating internal use; subclasses may access, but external code should not. - Private & Name Mangling (
__var): Python renames__varto_ClassName__varto prevent accidental name collisions in subclasses.
🐍 PythonInteractive WebAssemblyclass SecureVault:
def __init__(self, owner: str, passcode: str) -> None:
self.owner = owner # Public
self._security_level = 3 # Protected (developer agreement)
self.__passcode = passcode # Private (name mangled)
def verify_code(self, code: str) -> bool:
return self.__passcode == code
vault = SecureVault("Batman", "batmobile123")
print(vault.owner) # "Batman"
print(vault._security_level) # 3 (accessible, but discouraged)
# Trying to access __passcode directly raises AttributeError:
# print(vault.__passcode) # INCORRECT: AttributeError: 'SecureVault' object has no attribute '__passcode'
# Name mangled access (accessible if needed for debugging/serialization):
print(vault._SecureVault__passcode) # "batmobile123"
4.2 Getters, Setters & Deleters with @property#
The @property decorator allows you to define methods that can be accessed like attributes while providing validation, lazy evaluation, or computed values.
🐍 PythonInteractive WebAssemblyclass TemperatureSensor:
def __init__(self, celsius: float = 0.0) -> None:
self._celsius = celsius
# Getter
@property
def celsius(self) -> float:
"""The temperature in Celsius."""
return self._celsius
# Setter with strict data validation
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Temperature below absolute zero (-273.15°C) is impossible!")
self._celsius = float(value)
# Read-only computed property (Fahrenheit)
@property
def fahrenheit(self) -> float:
return (self._celsius * 9 / 5) + 32
# Deleter
@celsius.deleter
def celsius(self) -> None:
print("Resetting sensor reading to 0.0°C")
self._celsius = 0.0
sensor = TemperatureSensor(25.0)
print(sensor.celsius) # 25.0
print(sensor.fahrenheit) # 77.0
sensor.celsius = 100.0 # Uses setter
print(sensor.fahrenheit) # 212.0
try:
sensor.celsius = -300 # Raises ValueError
except ValueError as e:
print(f"Caught error: {e}")
5. Inheritance, Polymorphism & super()#
5.1 Single Inheritance & super()#
Inheritance allows a child class to inherit attributes and methods from a parent class, promoting code reuse.
🐍 PythonInteractive WebAssemblyclass PaymentProcessor:
def __init__(self, currency: str = "USD") -> None:
self.currency = currency
def process_payment(self, amount: float) -> str:
raise NotImplementedError("Subclasses must implement process_payment")
def refund(self, transaction_id: str) -> str:
return f"Refunding transaction {transaction_id} in {self.currency}"
class StripeProcessor(PaymentProcessor):
def __init__(self, api_key: str, currency: str = "USD") -> None:
# Call parent's __init__ using super()
super().__init__(currency=currency)
self.api_key = api_key
def process_payment(self, amount: float) -> str:
return f"Charged {amount} {self.currency} via Stripe (API Key: {self.api_key[:4]}***)"
class PayPalProcessor(PaymentProcessor):
def __init__(self, client_id: str, client_secret: str, currency: str = "USD") -> None:
super().__init__(currency=currency)
self.client_id = client_id
self.client_secret = client_secret
def process_payment(self, amount: float) -> str:
return f"Charged {amount} {self.currency} via PayPal account {self.client_id}"
5.2 Polymorphism & Duck Typing#
Polymorphism means "many forms". In Python, polymorphism is driven by Duck Typing:
"If it walks like a duck and quacks like a duck, it's a duck."
You do not need an explicit shared base class if the objects conform to the required interface:
🐍 PythonInteractive WebAssemblydef checkout(processor: PaymentProcessor, amount: float) -> None:
# Any object with a .process_payment() method will work seamlessly!
result = processor.process_payment(amount)
print(f"[SUCCESS] {result}")
stripe = StripeProcessor(api_key="sk_live_9482938492")
paypal = PayPalProcessor(client_id="paypal_merchant_1", client_secret="secret_xyz")
checkout(stripe, 99.99)
checkout(paypal, 49.50)
5.3 Multiple Inheritance & Method Resolution Order (MRO)#
Python supports multiple inheritance. The lookup sequence for methods is governed by the C3 Linearization Algorithm (accessible via Class.__mro__ or Class.mro()).
🐍 PythonInteractive WebAssemblyclass LoggerMixin:
def log(self, message: str) -> None:
print(f"[LOG {self.__class__.__name__}]: {message}")
class JSONSerializableMixin:
def to_json(self) -> str:
import json
return json.dumps(self.__dict__)
class DatabaseRecord(LoggerMixin, JSONSerializableMixin):
def __init__(self, table: str, record_id: int) -> None:
self.table = table
self.record_id = record_id
self.log(f"Initialized record {record_id} in {table}")
record = DatabaseRecord("users", 101)
print(record.to_json())
print(DatabaseRecord.mro())
# [<class '__main__.DatabaseRecord'>, <class '__main__.LoggerMixin'>, <class '__main__.JSONSerializableMixin'>, <class 'object'>]
The Diamond Problem Resolved with super()
When multiple parent classes inherit from the same grandparent, super() guarantees each class is initialized exactly once in cooperative multiple inheritance:
mermaidgraph TD A["Base Class A"] A --> B["Class B (super)"] A --> C["Class C (super)"] B --> D["Class D"] C --> D
🐍 PythonInteractive WebAssemblyclass A:
def action(self):
print("A action")
class B(A):
def action(self):
print("B start")
super().action()
print("B end")
class C(A):
def action(self):
print("C start")
super().action()
print("C end")
class D(B, C):
def action(self):
print("D start")
super().action()
print("D end")
d = D()
d.action()
# Output follows MRO: D -> B -> C -> A
6. Abstraction: Abstract Base Classes (ABCs) & Protocols#
6.1 Abstract Base Classes (abc.ABC)#
ABCs enforce that derived subclasses must implement specific abstract methods before they can be instantiated.
🐍 PythonInteractive WebAssemblyfrom abc import ABC, abstractmethod
class AsyncDatabaseDriver(ABC):
"""Abstract interface for database connection drivers."""
@abstractmethod
async def connect(self, dsn: str) -> None:
"""Establish database connection."""
pass
@abstractmethod
async def execute(self, query: str, *params) -> list[dict]:
"""Execute query and return records."""
pass
@property
@abstractmethod
def is_connected(self) -> bool:
"""Return connection health status."""
pass
class PostgresDriver(AsyncDatabaseDriver):
def __init__(self) -> None:
self._connected = False
async def connect(self, dsn: str) -> None:
print(f"Connecting to PostgreSQL at {dsn}")
self._connected = True
async def execute(self, query: str, *params) -> list[dict]:
return [{"id": 1, "query": query}]
@property
def is_connected(self) -> bool:
return self._connected
# Attempting to instantiate an incomplete subclass raises TypeError:
# class IncompleteDriver(AsyncDatabaseDriver): pass
# drv = IncompleteDriver() # INCORRECT: TypeError: Can't instantiate abstract class IncompleteDriver with abstract methods
6.2 Structural Subtyping with typing.Protocol (Static Duck Typing)#
Unlike ABCs which require explicit inheritance (class Sub(ParentABC)), Protocol enables compile-time type-checked duck typing.
🐍 PythonInteractive WebAssemblyfrom typing import Protocol, runtime_checkable
@runtime_checkable
class SupportsRender(Protocol):
def render(self) -> str:
...
class HTMLCard:
def __init__(self, title: str):
self.title = title
def render(self) -> str: # No inheritance needed!
return f"<div class='card'>{self.title}</div>"
card = HTMLCard("Dashboard")
print(isinstance(card, SupportsRender)) # True (runtime verified!)
7. Magic (Dunder) Methods Reference#
Dunder (Double Underscore) methods allow your custom objects to integrate natively with Python syntax operators and built-in functions.
7.1 String Representation: __repr__ vs __str__#
__repr__: Unambiguous developer representation (ideally valid Python code to recreate the object). Called byrepr(), interactive consoles, and debugger.__str__: Human-readable representation for end users. Called bystr()andprint().
🐍 PythonInteractive WebAssemblyclass Vector2D:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Vector2D(x={self.x!r}, y={self.y!r})"
def __str__(self) -> str:
return f"({self.x}, {self.y})"
v = Vector2D(3.5, 7.2)
print(str(v)) # "(3.5, 7.2)"
print(repr(v)) # "Vector2D(x=3.5, y=7.2)"
7.2 Equality, Hashing & Comparisons#
To use objects in set or as dict keys, they must implement __eq__ and __hash__.
🐍 PythonInteractive WebAssemblyfrom functools import total_ordering
@total_ordering # Automatically fills in __le__, __gt__, __ge__ from __eq__ and __lt__
class Task:
def __init__(self, title: str, priority: int) -> None:
self.title = title
self.priority = priority
def __eq__(self, other: object) -> bool:
if not isinstance(other, Task):
return NotImplemented
return (self.title, self.priority) == (other.title, other.priority)
def __lt__(self, other: object) -> bool:
if not isinstance(other, Task):
return NotImplemented
return self.priority < other.priority
def __hash__(self) -> int:
return hash((self.title, self.priority))
t1 = Task("Deploy v2", 1)
t2 = Task("Deploy v2", 1)
t3 = Task("Refactor auth", 2)
print(t1 == t2) # True
print(t1 < t3) # True
print(len({t1, t2})) # 1 (Set deduplication works via hash + eq)
7.3 Operator Overloading#
🐍 PythonInteractive WebAssemblyclass Money:
def __init__(self, amount: float, currency: str = "USD") -> None:
self.amount = round(amount, 2)
self.currency = currency
def __add__(self, other: "Money") -> "Money":
if not isinstance(other, Money) or self.currency != other.currency:
raise TypeError("Cannot add money of different currencies or non-Money types.")
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other: "Money") -> "Money":
if not isinstance(other, Money) or self.currency != other.currency:
raise TypeError("Mismatch in currency subtraction.")
return Money(self.amount - other.amount, self.currency)
def __mul__(self, factor: float) -> "Money":
return Money(self.amount * factor, self.currency)
def __repr__(self) -> str:
return f"{self.currency} {self.amount:.2f}"
m1 = Money(150.50)
m2 = Money(49.50)
print(m1 + m2) # USD 200.00
print(m1 * 2) # USD 301.00
7.4 Container / Collection Emulation#
Implement __len__, __getitem__, __setitem__, and __contains__ to create custom sequence or mapping collections:
🐍 PythonInteractive WebAssemblyclass CustomDataSet:
def __init__(self, data: list) -> None:
self._data = list(data)
def __len__(self) -> int:
return len(self._data)
def __getitem__(self, index: int | slice):
return self._data[index]
def __setitem__(self, index: int, value) -> None:
self._data[index] = value
def __contains__(self, item) -> bool:
return item in self._data
def __iter__(self):
return iter(self._data)
ds = CustomDataSet([10, 20, 30, 40, 50])
print(len(ds)) # 5
print(ds[1:4]) # [20, 30, 40] (Slicing supported!)
print(30 in ds) # True
print([x * 2 for x in ds]) # [20, 40, 60, 80, 100]
7.5 Callable Instances (__call__) & Context Managers (__enter__ / __exit__)#
🐍 PythonInteractive WebAssemblyimport time
# 1. Callable instance (Function object / Functor)
class ExponentialBackoff:
def __init__(self, base_delay: float = 1.0, factor: float = 2.0):
self.base_delay = base_delay
self.factor = factor
self.attempts = 0
def __call__(self) -> float:
delay = self.base_delay * (self.factor ** self.attempts)
self.attempts += 1
return delay
backoff = ExponentialBackoff()
print(backoff()) # 1.0
print(backoff()) # 2.0
print(backoff()) # 4.0
# 2. Context Manager Protocol
class TimerBlock:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f"Elapsed time: {self.elapsed * 1000:.2f} ms")
return False # Do not suppress exceptions
with TimerBlock():
sum(range(1_000_000))
8. Memory Optimization with __slots__#
By default, every Python instance stores its attributes in a dynamic dictionary (self.__dict__). While flexible, this adds significant memory overhead (150+ bytes per instance).
Defining __slots__ allocates a fixed array of attribute pointers, reducing memory consumption by 50% to 70% and preventing arbitrary attribute assignment.
🐍 PythonInteractive WebAssemblyimport sys
class StandardCoordinate:
def __init__(self, x: float, y: float, z: float):
self.x = x
self.y = y
self.z = z
class SlottedCoordinate:
__slots__ = ("x", "y", "z") # No __dict__ created!
def __init__(self, x: float, y: float, z: float):
self.x = x
self.y = y
self.z = z
p1 = StandardCoordinate(1.0, 2.0, 3.0)
p2 = SlottedCoordinate(1.0, 2.0, 3.0)
# Memory footprint comparison:
print(f"Standard instance + dict size: {sys.getsizeof(p1) + sys.getsizeof(p1.__dict__)} bytes")
print(f"Slotted instance size: {sys.getsizeof(p2)} bytes")
# Disallows dynamic attribute attachment:
# p2.label = "Origin" # INCORRECT: AttributeError: 'SlottedCoordinate' object has no attribute 'label'
When creating millions of lightweight data objects (e.g., in data processing pipelines, geometry engines, graph nodes), always consider
__slots__or@dataclass(slots=True).
9. Descriptors Protocol: The Engine Behind @property and Methods#
A descriptor is any object that implements at least one of __get__, __set__, or __delete__. Descriptors customize attribute access lookup.
🐍 PythonInteractive WebAssemblyclass PositiveNumber:
"""Descriptor that enforces positive numeric values."""
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name, 0.0)
def __set__(self, instance, value):
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(f"'{self.public_name}' must be a positive number.")
setattr(instance, self.private_name, value)
class Product:
# Descriptors attached at class level
price = PositiveNumber()
weight = PositiveNumber()
def __init__(self, name: str, price: float, weight: float):
self.name = name
self.price = price
self.weight = weight
item = Product("Laptop", 1299.99, 1.8)
print(item.price) # 1299.99
try:
item.price = -50 # INCORRECT: ValueError: 'price' must be a positive number.
except ValueError as e:
print(e)
10. Modern Python: @dataclass#
Introduced in Python 3.7 (and enhanced in 3.10+), @dataclass eliminates boilerplate code for __init__, __repr__, __eq__, and comparisons.
🐍 PythonInteractive WebAssemblyfrom dataclasses import dataclass, field
from typing import List
@dataclass(order=True, slots=True)
class MLModelArtifact:
# Sort order compares fields in declared sequence
accuracy: float
model_name: str = field(compare=False)
parameters_count: int = field(compare=False)
tags: List[str] = field(default_factory=list, compare=False)
def __post_init__(self):
"""Validation or post-processing executed after generated __init__."""
if not (0.0 <= self.accuracy <= 1.0):
raise ValueError("Accuracy must be between 0.0 and 1.0")
m1 = MLModelArtifact(accuracy=0.945, model_name="ResNet50", parameters_count=25_000_000)
m2 = MLModelArtifact(accuracy=0.982, model_name="ViT-Large", parameters_count=300_000_000)
print(m1) # MLModelArtifact(accuracy=0.945, model_name='ResNet50', parameters_count=25000000, tags=[])
print(m2 > m1) # True (Compared by accuracy automatically!)
When to Use What?#
| Feature | Standard class | @dataclass | typing.NamedTuple | Pydantic BaseModel |
|---|---|---|---|---|
| Primary Goal | Custom stateful logic & behaviors | Clean data carrier with methods | Immutable tuple with named fields | Complex serialization & API parsing |
| Mutability | Mutable | Configurable (frozen=True) | Immutable | Configurable |
| Overhead | Standard | Very low (slots=True) | Ultra low (C-tuple based) | Parsing/Validation overhead |
| Validation | Manual in __init__ | __post_init__ | None at runtime | Rich built-in validators |
11. SOLID Principles in Python#
The SOLID principles guide clean, maintainable, and scalable object-oriented software design:
1. Single Responsibility Principle (SRP)#
A class should have one, and only one, reason to change.
🐍 PythonInteractive WebAssembly# INCORRECT: BAD: Class handles data management, formatting, and disk persistence
class ReportBad:
def generate_data(self): ...
def format_html(self): ...
def save_to_s3(self): ...
# CORRECT: GOOD: Separated into dedicated components
class ReportData:
def fetch_metrics(self) -> dict: ...
class ReportHTMLFormatter:
def format(self, data: dict) -> str: ...
class S3Uploader:
def upload(self, content: str, bucket: str) -> None: ...
2. Open/Closed Principle (OCP)#
Classes should be open for extension, but closed for modification.
🐍 PythonInteractive WebAssemblyfrom abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def apply_discount(self, total: float) -> float:
pass
class RegularDiscount(DiscountStrategy):
def apply_discount(self, total: float) -> float:
return total
class VIPDiscount(DiscountStrategy):
def apply_discount(self, total: float) -> float:
return total * 0.80 # 20% discount
# Adding a new discount does not modify existing checkout logic!
class BlackFridayDiscount(DiscountStrategy):
def apply_discount(self, total: float) -> float:
return total * 0.50
3. Liskov Substitution Principle (LSP)#
Subtypes must be substitutable for their base types without breaking client code.
🐍 PythonInteractive WebAssembly# INCORRECT: BAD: Square breaks Rectangle's behavioral invariants
class Rectangle:
def set_width(self, w: float): self.w = w
def set_height(self, h: float): self.h = h
class SquareBad(Rectangle):
def set_width(self, w: float): self.w = self.h = w
# CORRECT: GOOD: Use common geometric shape abstraction
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
4. Interface Segregation Principle (ISP)#
Clients should not be forced to depend on interfaces they do not use.
🐍 PythonInteractive WebAssembly# INCORRECT: BAD: Fat monolithic interface
class Worker(ABC):
@abstractmethod
def code(self): pass
@abstractmethod
def test(self): pass
@abstractmethod
def design_ui(self): pass
# CORRECT: GOOD: Focused role interfaces
class Programmer(ABC):
@abstractmethod
def code(self): pass
class Tester(ABC):
@abstractmethod
def test(self): pass
5. Dependency Inversion Principle (DIP)#
High-level modules should not depend on low-level modules; both should depend on abstractions.
🐍 PythonInteractive WebAssemblyclass NotificationSender(ABC):
@abstractmethod
def send(self, recipient: str, message: str) -> None:
pass
class EmailSender(NotificationSender):
def send(self, recipient: str, message: str) -> None:
print(f"Sending Email to {recipient}: {message}")
class OrderService:
# Injects abstraction rather than hardcoding concrete EmailSender
def __init__(self, notifier: NotificationSender) -> None:
self.notifier = notifier
def complete_order(self, customer_email: str, order_id: str) -> None:
# Business logic...
self.notifier.send(customer_email, f"Order #{order_id} confirmed!")
12. Classic OOP Design Patterns in Python#
12.1 Singleton Pattern (Thread-Safe Metaclass)#
Ensures a class has only one instance and provides a global point of access.
🐍 PythonInteractive WebAssemblyimport threading
class SingletonMeta(type):
"""Thread-safe Singleton implementation via metaclass."""
_instances = {}
_lock: threading.Lock = threading.Lock()
def __call__(cls, *args, **kwargs):
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class ApplicationConfig(metaclass=SingletonMeta):
def __init__(self) -> None:
self.database_url = "postgresql://localhost:5432/production"
self.debug_mode = False
c1 = ApplicationConfig()
c2 = ApplicationConfig()
print(c1 is c2) # True (Exact same object in memory)
12.2 Factory Pattern#
Provides an interface for creating objects in a superclass while allowing subclasses to alter the type of objects that will be created.
🐍 PythonInteractive WebAssemblyclass StorageService(ABC):
@abstractmethod
def save(self, filename: str, data: bytes) -> str:
pass
class S3Storage(StorageService):
def save(self, filename: str, data: bytes) -> str:
return f"s3://my-bucket/{filename}"
class LocalStorage(StorageService):
def save(self, filename: str, data: bytes) -> str:
return f"/var/data/uploads/{filename}"
class StorageFactory:
@staticmethod
def get_storage(environment: str) -> StorageService:
match environment.lower():
case "production" | "cloud":
return S3Storage()
case "development" | "local":
return LocalStorage()
case _:
raise ValueError(f"Unknown storage environment: {environment}")
storage = StorageFactory.get_storage("production")
print(storage.save("avatar.png", b"...")) # "s3://my-bucket/avatar.png"
13. Summary & Quick Reference Cheat Sheet#
| Task | Syntax / Method | Purpose |
|---|---|---|
| Constructor allocation | def __new__(cls, *args) | Allocates and returns new object memory |
| Instance initialization | def __init__(self, *args) | Sets initial attribute state |
| Alternative constructor | @classmethod def factory(cls) | Returns a new instance from alternative arguments |
| Namespaced utility | @staticmethod def helper() | Independent helper function tied to class |
| Encapsulated property | @property / @x.setter | Computed/validated attribute access |
| Developer printout | def __repr__(self) | Unambiguous debug string (repr(obj)) |
| User display | def __str__(self) | Friendly formatted string (str(obj)) |
| Callable instance | def __call__(self, *args) | Allows calling instance like a function (obj()) |
| Context manager | __enter__ and __exit__ | Manages resources with with blocks |
| Memory optimization | __slots__ = ("a", "b") | Prevents __dict__ overhead |
| Explicit contracts | from abc import ABC, abstractmethod | Enforces subclass implementation |
| Structural typing | from typing import Protocol | Static Duck Typing for interface checking |
Object-Oriented Programming (OOP) Checkpoint
Finished studying this notebook?
Mark this guide as completed to update your course progress roadmap.