domain: https://code-sense.pansensoyglenn.workers.dev — full OOP architecture guide with Python deep dives
of Python codebases use OOP extensively
reduction in bugs with SOLID compliance
faster refactoring with design patterns
In professional engineering, encapsulation is the primary mechanism for maintaining data invariants.
By restricting direct access to an object's state, we ensure that the object remains in a valid configuration throughout its lifecycle.
Python implements this via naming conventions: _protected for internal scope (developer convention) and
__private for name mangling (prevents accidental override in subclasses).
Leveraging @property decorators allows for "controlled access," where logic can be injected during attribute
retrieval or assignment. This is where invariants are enforced (e.g., non‑negative balance, valid email format).
Below a robust example with getter/setter and a hidden attribute.
class UserAccount:
def __init__(self, email: str, initial_balance: float = 0):
self._email = None
self.__balance = 0
self.email = email
self.balance = initial_balance
@property
def email(self) -> str:
return self._email
@email.setter
def email(self, value: str):
if '@' not in value or '.' not in value.split('@')[-1]:
raise ValueError("Invalid email format")
self._email = value
@property
def balance(self) -> float:
return self.__balance
@balance.setter
def balance(self, value: float):
if value < 0:
raise ValueError("Balance cannot be negative")
if value > 1000000:
raise ValueError("Balance exceeds maximum limit")
self.__balance = value
def deposit(self, amount: float):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
🔬 Performance Consideration: Using @property adds ~0.5μs overhead per access. For millions of operations, consider direct attribute access or __slots__.
Invariant enforcement: the setter guarantees that balance never goes below zero and email is always valid.
The mangled name __balance discourages direct manipulation from outside.
🔍 Deep insight: Name mangling exists primarily to avoid accidental attribute collisions in subclasses, not for security. Real encapsulation relies on conventions and properties — trust the developer, protect the invariant.
Certified software systems must adhere to the SOLID framework to ensure maintainability, testability, and resilience to shifting requirements. Each principle addresses a specific fragility.
A module should encapsulate one business concern. Avoid "God Objects" that manage multiple domains. Example: separate ReportGenerator from ReportPrinter.
Entities should be open for extension, closed for modification. Achieved via polymorphism / strategy pattern. Add new features by writing new code, not altering tested existing code.
Derived classes must be substitutable for their base classes without altering correctness. If a function works with Bird, it must work with Penguin (even if penguins don't fly — maybe redesign).
Clients should not be forced to depend on methods they do not use. Split large interfaces into smaller, focused ones (e.g., Printer and Scanner instead of AllInOne).
High-level modules should depend on abstract interfaces, not concrete implementations. This facilitates unit testing (mocking) and decoupling. DatabaseService depends on StorageInterface, not MySQLConnection.
class Storage(ABC):
@abstractmethod def save(data): ...
class S3Storage(Storage): ...
class LocalStorage(Storage): ...
class DataService:
def __init__(self, storage: Storage):
self._storage = storage
| Principle | Violation | Fix |
|---|---|---|
| SRP | Class handling both auth and logging | Split into AuthService + Logger |
| OCP | if-else chains for new features | Use strategy pattern |
| LSP | Square inheriting Rectangle | Use common Shape interface |
| ISP | Fat interfaces with unused methods | Split into smaller interfaces |
| DIP | High-level module imports low-level | Inject abstractions |
While inheritance establishes an "Is-A" relationship, it often leads to rigid hierarchies and fragile base class problems. Senior architects favor Composition (the "Has-A" relationship) to inject behavior dynamically at runtime, improving flexibility and testability.
Mixins are a special form of composition via multiple inheritance that add reusable chunks of behavior. Dependency injection is a composition technique where required collaborators are passed in (often through constructor).
class Notifier:
def send(self, message): pass
class EmailNotifier(Notifier):
def send(self, message): print(f"Email: {message}")
class SMSNotifier(Notifier):
def send(self, message): print(f"SMS: {message}")
class LoggingMixin:
def log(self, msg): print(f"LOG: {msg}")
class AlertService(LoggingMixin):
def __init__(self, notifiers: List[Notifier]):
self._notifiers = notifiers
def alert(self, message):
self.log("Sending alert")
for notifier in self._notifiers:
notifier.send(message)
# Compose at runtime
service = AlertService([EmailNotifier(), SMSNotifier()])
Benefits: loose coupling, easier mocking (pass a fake pool), behavior reuse without deep hierarchy.
🎯 Rule of thumb: "Prefer composition over inheritance" unless you need polymorphic behavior AND code reuse through an "is-a" relationship.
Design patterns provide a standardized vocabulary for solving recurring structural problems. Creational patterns abstract the instantiation process, making the system independent of how objects are created.
class SQLQueryBuilder:
def __init__(self):
self._query = ""
def select(self, *fields):
self._query += f"SELECT {', '.join(fields)} "
return self
def from_table(self, table):
self._query += f"FROM {table} "
return self
def where(self, condition):
self._query += f"WHERE {condition} "
return self
def build(self) -> str:
return self._query.strip()
# Usage
query = (SQLQueryBuilder()
.select("name", "email")
.from_table("users")
.where("age > 18")
.build())
Registry + Factory example:
class Document: pass
class PDFDoc(Document): ...
class HTMLDoc(Document): ...
class DocumentFactory:
_registry = {}
@classmethod def register(cls, ext, builder):
cls._registry[ext] = builder
@classmethod def create(cls, ext, title):
return cls._registry[ext](title)
DocumentFactory.register('pdf', PDFDoc)
doc = DocumentFactory.create('pdf', 'report')
In high-throughput systems, the default Python __dict__ overhead is non-trivial.
Using __slots__ allows for fixed attribute allocation, significantly reducing the memory footprint
per instance in data-intensive applications (e.g., millions of objects). Additionally, weakref enables
references that don't prevent garbage collection — crucial for caches.
| Technique | Memory per 1M objects | Access Speed |
|---|---|---|
| Regular class (__dict__) | ~80 MB | Baseline |
| __slots__ | ~40 MB (50% reduction) | 15% faster |
| NamedTuple | ~32 MB | 20% faster |
| dataclass + slots=True | ~42 MB | 15% faster |
class Point:
__slots__ = ('x', 'y', 'z')
def __init__(self, x, y, z):
self.x, self.y, self.z = x, y, z
Weak references avoid memory leaks in observer patterns or caches:
import weakref
class Observer: ...
class Subject:
def __init__(self):
self._observers = weakref.WeakSet()
⚡ Pro tip: Use __slots__ when creating >100,000 instances. For smaller counts, the maintenance overhead isn't worth it.
Polymorphism enables objects of different types to respond to the same interface. Python excels with
duck typing: “if it walks like a duck and quacks like a duck, it's a duck”.
Abstract Base Classes (ABC) formalize interfaces and can enforce method implementation.
from abc import ABC, abstractmethod
class DataProcessor(ABC):
@abstractmethod
def process(self, data):
pass
@abstractmethod
def validate(self, data):
pass
class CSVProcessor(DataProcessor):
def process(self, data):
return [row.split(',') for row in data.split('\n')]
def validate(self, data):
return all(',' in row for row in data.split('\n'))
class JSONProcessor(DataProcessor):
def process(self, data):
import json
return json.loads(data)
def validate(self, data):
try:
import json; json.loads(data)
return True
except:
return False
# Polymorphic dispatch
def handle_data(processor: DataProcessor, raw_data):
if processor.validate(raw_data):
return processor.process(raw_data)
raise ValueError("Invalid data format")
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod def area(self): pass
class Circle(Shape):
def __init__(self, radius): self.r = radius
def area(self): return 3.14 * self.r ** 2
class Duck:
def quack(self): print("quack")
class Person:
def quack(self): print("i try to quack")
def make_it_quack(thing): thing.quack()
Behavioral patterns focus on communication between objects, encapsulating varying behavior.
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount): pass
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number):
self.card_number = card_number
def pay(self, amount):
print(f"Paid ${amount} with credit card {self.card_number[-4:]}")
class PayPalPayment(PaymentStrategy):
def __init__(self, email):
self.email = email
def pay(self, amount):
print(f"Paid ${amount} via PayPal account {self.email}")
class ShoppingCart:
def __init__(self, payment_strategy: PaymentStrategy):
self.items = []
self.payment_strategy = payment_strategy
def checkout(self):
total = sum(item.price for item in self.items)
self.payment_strategy.pay(total)
# Usage
cart = ShoppingCart(CreditCardPayment("1234-5678-9012-3456"))
cart.checkout()
Observer minimal example:
class Subject:
def __init__(self): self.obs = []
def attach(self, o): self.obs.append(o)
def notify(self):
for o in self.obs: o.update(self)
class ConcreteObserver:
def update(self, subject): print("updated")
Strategy and Template are widely used in frameworks — e.g., Django's class‑based views use template method (get, post hooks).
Design by Contract (DbC) specifies formal, precise and verifiable interface specifications.
In Python, we enforce contracts using assertions, exceptions, or libraries (e.g., icontract).
Preconditions (what must be true before a method runs), postconditions (what must be true after),
and class invariants (always true) increase reliability.
class BankAccount:
def __init__(self, owner, initial_balance):
self.owner = owner
self._balance = 0
assert initial_balance >= 0, "Initial balance must be non-negative"
self._balance = initial_balance
self._invariant()
def _invariant(self):
assert self._balance >= 0, "Invariant violated: balance negative"
def withdraw(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
if amount > self._balance:
raise ValueError("Insufficient funds")
old_balance = self._balance
self._balance -= amount
assert self._balance == old_balance - amount
self._invariant()
return self._balance
🧱 Architectural note: Explicit contracts serve as built‑in documentation and early error detection. They are especially valuable in long‑lived, critical systems.