The Definitive Software Design Patterns Compendium
Explore 28 deep architectural chapters, 5 foundational tiers, and 38 runnable Python 3.14 implementations with real-world sightings from Django, FastAPI, SQLAlchemy, and Celery. Built with sub-millisecond diagram workbenches, crystal glass aesthetics, and haptic acoustic feedback.
Chapter 00: Foundations
The bedrock every design pattern stands on. Master these principles first — every pattern in this repository is a specific application of one or more of these ideas.
🧒 The 5-Year-Old Explanation
Imagine you're building with LEGO. Before you follow any instruction booklet (patterns), you need to know some basic rules:
- Don't glue bricks together (keep things changeable — that's the Open/Closed Principle)
- Each brick does ONE thing (a wheel is a wheel, a window is a window — Single Responsibility)
- Big bricks and small bricks should snap together the same way (Liskov Substitution)
- Don't carry bricks you're not using (Interface Segregation)
- Follow the picture on the box, not the factory that made the bricks (Dependency Inversion)
📦 What This Chapter Covers
| Topic | Why It Matters |
|---|---|
| SOLID Principles | The 5 laws of object-oriented design — every pattern embodies at least one |
| DRY, KISS, YAGNI | Three mantras that prevent over-engineering |
| Composition vs Inheritance | The #1 design decision you'll make repeatedly |
| UML Cheat Sheet | How to read the class and sequence diagrams used throughout this repo |
| Pattern Categories | Mental map of all 26 chapters |
🏛️ SOLID Principles
The Big Picture
S — Single Responsibility Principle (SRP)
"A class should have only one reason to change." — Robert C. Martin
🧒 5-Year-Old Version
A firefighter fights fires. A doctor treats patients. If one person had to do BOTH jobs, they'd be terrible at both and exhausted. Each person has ONE job.The Rule
Every class should be responsible for **one thing**. If you can describe what a class does and you use the word "AND", it probably has too many responsibilities.UML: Before vs After
Which Patterns Embody SRP?
| Pattern | How It Uses SRP |
|---|---|
| Strategy (Ch 20) | Extracts algorithm into its own class |
| Command (Ch 14) | Extracts action into its own object |
| Observer (Ch 18) | Separates event source from event handlers |
| Chain of Responsibility (Ch 13) | Each handler does one thing |
O — Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification." — Bertrand Meyer
🧒 5-Year-Old Version
A phone case: you can change the case (extension) without opening up and rewiring the phone (modification). The phone is "closed" — you don't touch its guts. But it's "open" — you can add new cases, screen protectors, and pop sockets.The Rule
You should be able to add new behavior **without changing existing code**. Achieve this through abstraction (interfaces, abstract classes) and polymorphism.Flow: Violation → Fix
Which Patterns Embody OCP?
| Pattern | How It Uses OCP |
|---|---|
| Strategy (Ch 20) | New algorithms = new classes, no modification |
| Decorator (Ch 09) | New behavior wraps existing, no modification |
| Factory Method (Ch 02) | New products = new factory subclasses |
| Template Method (Ch 21) | New steps = new subclasses, skeleton unchanged |
L — Liskov Substitution Principle (LSP)
"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application." — Barbara Liskov
🧒 5-Year-Old Version
If you ask for a "bird" and someone gives you a penguin, you shouldn't be surprised when it can't fly. If `fly()` is promised by "Bird", then penguins shouldn't be birds in your code. Don't make promises your children can't keep.The Rule
A subclass must be able to **stand in** for its parent class. If substituting a subclass breaks the program, you've violated LSP. The classic violator: `Square extends Rectangle` (changing width also changes height — surprise!).UML: The Classic Violation
Which Patterns Embody LSP?
| Pattern | How It Uses LSP |
|---|---|
| Abstract Factory (Ch 03) | All factories are interchangeable |
| Strategy (Ch 20) | All strategies are interchangeable |
| State (Ch 19) | All states are interchangeable |
| Composite (Ch 08) | Leaf and Composite both implement Component |
I — Interface Segregation Principle (ISP)
"No client should be forced to depend on methods it does not use." — Robert C. Martin
🧒 5-Year-Old Version
Imagine a TV remote with 200 buttons — but you only use 5. All those extra buttons are confusing and wasteful. Better to have a simple remote with just the buttons you need.The Rule
Split fat interfaces into smaller, focused ones. A class should only need to implement the methods it actually uses.UML: Fat vs Lean Interfaces
Which Patterns Embody ISP?
| Pattern | How It Uses ISP |
|---|---|
| Adapter (Ch 06) | Exposes only the interface the client needs |
| Facade (Ch 10) | Hides complex subsystem behind simple interface |
| Proxy (Ch 12) | Same interface, controlled access |
D — Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions." — Robert C. Martin
🧒 5-Year-Old Version
When you plug in a lamp, you don't wire it directly to the power plant. You plug it into a **wall outlet** (the abstraction). The lamp doesn't know or care if the electricity comes from solar, wind, or coal. And the power plant doesn't know or care if it's powering a lamp, a fridge, or a TV.The Rule
Don't import concrete classes directly. Depend on interfaces/protocols. This is the foundation of **Dependency Injection** (Chapter 25).Architecture: Before vs After
Which Patterns Embody DIP?
| Pattern | How It Uses DIP |
|---|---|
| Dependency Injection (Ch 25) | THE pattern for DIP |
| Abstract Factory (Ch 03) | Client depends on factory interface, not concrete factories |
| Bridge (Ch 07) | Abstraction depends on implementor interface |
| Repository (Ch 23) | Business logic depends on repository interface, not DB |
🧘 Three Mantras: DRY, KISS, YAGNI
| Mantra | One-Liner | Anti-Example |
|---|---|---|
| DRY | If you're copy-pasting, you're doing it wrong | Same validation logic in 5 different endpoints |
| KISS | If a junior dev can't understand it in 5 minutes, simplify | Using a 3-level metaclass hierarchy when a simple function works |
| YAGNI | Build for today, design for tomorrow | Building a plugin system when you have 1 plugin |
⚠️ Tension Alert: These mantras can conflict. DRY might push you to create abstractions that violate KISS. YAGNI might stop you from applying a pattern that DRY demands. Use judgment. When in doubt: KISS > DRY > YAGNI.
⚔️ Composition vs Inheritance
This is the #1 most important design decision in OOP. The Gang of Four themselves said:
"Favor composition over inheritance." — GoF, Design Patterns (1994)
What's the Difference?
Side-by-Side Comparison
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship | IS-A (Dog IS-A Animal) | HAS-A (Dog HAS-A BarkBehavior) |
| Coupling | Tight — child depends on parent's implementation | Loose — components are independent |
| Flexibility | Fixed at compile time | Swappable at runtime |
| Hierarchy depth | Deep hierarchies become fragile | Flat, modular |
| Reusability | Subclass locked to one parent | Components reusable across unrelated classes |
| When to use | True IS-A relationship, shared implementation | Behavior varies, runtime flexibility needed |
Decision Flowchart
Which Patterns Use Which?
| Uses Inheritance | Uses Composition |
|---|---|
| Template Method (Ch 21) | Strategy (Ch 20) |
| Factory Method (Ch 02) | Decorator (Ch 09) |
| State (Ch 19) — state classes | Bridge (Ch 07) |
| Observer (Ch 18) | |
| Composite (Ch 08) | |
| Command (Ch 14) |
Rule of thumb: If you're torn, choose composition. You can always refactor to inheritance later if needed, but the reverse is painful.
📐 UML Cheat Sheet
Every chapter in this repository uses Mermaid UML diagrams. Here's how to read them.
Class Diagram Symbols
Relationship Arrows
| Arrow | Meaning | Python Equivalent |
|---|---|---|
──▷ (solid, closed) |
Inheritance | class Child(Parent) |
──▷ (dashed, closed) |
Realization/Implementation | class Concrete(ABC) or Protocol |
──> (solid, open) |
Association (permanent reference) | self.other = Other() |
..> (dashed, open) |
Dependency (temporary usage) | def method(other: Other) |
◇── (open diamond) |
Aggregation (has-a, independent lifetime) | self.items = items # passed in |
◆── (filled diamond) |
Composition (has-a, dependent lifetime) | self.engine = Engine() # created internally |
Sequence Diagram Basics
| Arrow | Meaning |
|---|---|
->> (solid) |
Synchronous call |
-->> (dashed) |
Return |
--) (solid, open) |
Async message |
activate/deactivate |
Lifeline (object is active) |
🗺️ Design Pattern Categories — Master Map
🔗 SOLID ↔ Pattern Cross-Reference
| SOLID Principle | Patterns That Embody It |
|---|---|
| S — Single Responsibility | Strategy, Command, Observer, Chain of Responsibility |
| O — Open/Closed | Strategy, Decorator, Factory Method, Template Method |
| L — Liskov Substitution | Abstract Factory, Strategy, State, Composite |
| I — Interface Segregation | Adapter, Facade, Proxy |
| D — Dependency Inversion | Dependency Injection, Abstract Factory, Bridge, Repository |
✅ Examples in This Chapter
| # | File | What It Demonstrates |
|---|---|---|
| 1 | solid_principles.py |
All 5 SOLID principles with violation → fix code |
| 2 | compositionvsinheritance.py |
Side-by-side comparison with real-world scenario |
⚡ Interactive Python 3.14 Implementations (2)
"""
============================================================
Example: Composition vs Inheritance — Side-by-Side
Pattern: Foundations
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
We're building a notification system for an e-commerce platform.
Notifications can be sent via Email, SMS, or Push. They can also
be urgent, scheduled, or batched. With inheritance, this creates
a combinatorial explosion of subclasses. With composition, it's
clean and extensible.
KEY INSIGHT:
------------
Inheritance creates a rigid hierarchy that explodes with every
new axis of variation. Composition lets you mix and match
behaviors like LEGO bricks — independently and at runtime.
============================================================
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
# ════════════════════════════════════════════════════════════
# ❌ INHERITANCE APPROACH — The Combinatorial Explosion
# ════════════════════════════════════════════════════════════
# Level 1: Base
class NotificationBad(ABC):
@abstractmethod
def send(self, message: str) -> None: ...
# Level 2: Channel (3 subclasses)
class EmailNotificationBad(NotificationBad):
def send(self, message: str) -> None:
print(f" 📧 Email: {message}")
class SMSNotificationBad(NotificationBad):
def send(self, message: str) -> None:
print(f" 📱 SMS: {message}")
class PushNotificationBad(NotificationBad):
def send(self, message: str) -> None:
print(f" 🔔 Push: {message}")
# Level 3: Urgency × Channel = 6 MORE subclasses (explosion begins!)
class UrgentEmailNotificationBad(EmailNotificationBad):
def send(self, message: str) -> None:
print(f" 🚨📧 URGENT Email: {message}")
class UrgentSMSNotificationBad(SMSNotificationBad):
def send(self, message: str) -> None:
print(f" 🚨📱 URGENT SMS: {message}")
class ScheduledEmailNotificationBad(EmailNotificationBad):
def send(self, message: str) -> None:
print(f" ⏰📧 Scheduled Email: {message}")
# ... and it keeps growing:
# BatchedEmailNotificationBad, BatchedSMSNotificationBad,
# ScheduledPushNotificationBad, UrgentBatchedEmailNotificationBad...
#
# With 3 channels × 3 urgency types × 3 delivery modes = 27 classes!
# Add logging? 54 classes. Add encryption? 108 classes. 💀
# ════════════════════════════════════════════════════════════
# ✅ COMPOSITION APPROACH — Mix and Match Like LEGO
# ════════════════════════════════════════════════════════════
# ─── Step 1: Define small, focused behaviors (protocols) ──
@runtime_checkable
class Channel(Protocol):
"""HOW to deliver the notification."""
def deliver(self, message: str) -> None: ...
@runtime_checkable
class UrgencyFormatter(Protocol):
"""HOW to format the urgency."""
def format(self, message: str) -> str: ...
class DeliveryMode(Protocol):
"""WHEN to deliver the notification."""
def schedule(self, message: str) -> str: ...
# ─── Step 2: Implement each axis independently ────────────
# Channels (3 classes)
class EmailChannel:
def deliver(self, message: str) -> None:
print(f" 📧 Email: {message}")
class SMSChannel:
def deliver(self, message: str) -> None:
print(f" 📱 SMS: {message}")
class PushChannel:
def deliver(self, message: str) -> None:
print(f" 🔔 Push: {message}")
# Urgency Formatters (3 classes)
class NormalUrgency:
def format(self, message: str) -> str:
return message
class UrgentUrgency:
def format(self, message: str) -> str:
return f"🚨 URGENT: {message}"
class LowPriorityUrgency:
def format(self, message: str) -> str:
return f"ℹ️ FYI: {message}"
# Delivery Modes (3 classes)
class ImmediateDelivery:
def schedule(self, message: str) -> str:
return f"[NOW] {message}"
class ScheduledDelivery:
def schedule(self, message: str) -> str:
return f"[SCHEDULED 9AM] {message}"
class BatchedDelivery:
def schedule(self, message: str) -> str:
return f"[BATCHED] {message}"
# ─── Step 3: Compose them ─────────────────────────────────
@dataclass
class Notification:
"""
Composes channel + urgency + delivery mode.
Instead of 27+ subclasses, we have:
- 3 channels + 3 urgency types + 3 delivery modes = 9 classes
- And 3 × 3 × 3 = 27 COMBINATIONS without any new classes!
Adding a new channel? Just 1 new class (not N × M new subclasses).
"""
channel: Channel
urgency: UrgencyFormatter
delivery: DeliveryMode
def send(self, message: str) -> None:
formatted = self.urgency.format(message)
scheduled = self.delivery.schedule(formatted)
self.channel.deliver(scheduled)
# ════════════════════════════════════════════════════════════
# COMPARISON TABLE (printed at runtime)
# ════════════════════════════════════════════════════════════
COMPARISON = """
╔══════════════════════════╦══════════════════════════════╗
║ INHERITANCE ║ COMPOSITION ║
╠══════════════════════════╬══════════════════════════════╣
║ 27+ classes for 3×3×3 ║ 9 classes + 1 compositor ║
║ Adding channel = N new ║ Adding channel = 1 new class ║
║ Fixed at class definition║ Swappable at runtime ║
║ Deep hierarchy = fragile ║ Flat, independent components ║
║ Hard to test in isolation║ Each component testable alone ║
╚══════════════════════════╩══════════════════════════════╝
"""
# ════════════════════════════════════════════════════════════
# DEMO
# ════════════════════════════════════════════════════════════
if __name__ == "__main__":
print("=" * 60)
print("❌ INHERITANCE APPROACH")
print("=" * 60)
print(" (Showing just 3 of the 27+ subclasses needed)")
EmailNotificationBad().send("Your order shipped!")
UrgentEmailNotificationBad().send("Payment failed!")
ScheduledEmailNotificationBad().send("Weekly digest")
print()
print("=" * 60)
print("✅ COMPOSITION APPROACH")
print("=" * 60)
print(" (Same 3 combinations — ZERO new subclasses)")
# Combination 1: Normal email, immediate
n1 = Notification(
channel=EmailChannel(),
urgency=NormalUrgency(),
delivery=ImmediateDelivery(),
)
n1.send("Your order shipped!")
# Combination 2: Urgent email, immediate
n2 = Notification(
channel=EmailChannel(),
urgency=UrgentUrgency(),
delivery=ImmediateDelivery(),
)
n2.send("Payment failed!")
# Combination 3: Normal email, scheduled
n3 = Notification(
channel=EmailChannel(),
urgency=NormalUrgency(),
delivery=ScheduledDelivery(),
)
n3.send("Weekly digest")
print()
print(" 🔄 Runtime swap — same Notification, different channel:")
# Swap channel at RUNTIME — impossible with inheritance!
n4 = Notification(
channel=SMSChannel(),
urgency=UrgentUrgency(),
delivery=ImmediateDelivery(),
)
n4.send("Server is down!")
n5 = Notification(
channel=PushChannel(),
urgency=LowPriorityUrgency(),
delivery=BatchedDelivery(),
)
n5.send("New blog post available")
print()
print(COMPARISON)
# Verify that our objects satisfy the protocols
assert isinstance(EmailChannel(), Channel) # type: ignore[arg-type]
assert isinstance(UrgentUrgency(), UrgencyFormatter) # type: ignore[arg-type]
print("✅ Composition vs Inheritance demonstrated successfully!")
"""
============================================================
Example: SOLID Principles — Violation → Fix
Pattern: Foundations
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
We model a simple e-commerce system with users, orders, emails,
and reports. Each SOLID principle is demonstrated by first showing
the VIOLATION (bad code), then the FIX (good code).
KEY INSIGHT:
------------
SOLID isn't abstract theory — it's the difference between code
that's a joy to extend and code that makes you dread Monday mornings.
============================================================
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
# ════════════════════════════════════════════════════════════
# S — SINGLE RESPONSIBILITY PRINCIPLE (SRP)
# ════════════════════════════════════════════════════════════
# ❌ BAD: One class does EVERYTHING
class UserManagerBad:
"""Handles user CRUD, emails, reports, AND logging. Too many jobs!"""
def create_user(self, name: str, email: str) -> dict:
user = {"name": name, "email": email}
# ... save to database ...
self._send_welcome_email(email) # SRP violation: why is email logic here?
self._log(f"Created user {name}") # SRP violation: why is logging here?
return user
def _send_welcome_email(self, email: str) -> None:
print(f"Sending welcome email to {email}")
def _log(self, message: str) -> None:
print(f"LOG: {message}")
def generate_user_report(self) -> str:
# SRP violation: report generation has nothing to do with user management
return "User Report: ..."
# ✅ GOOD: Each class has ONE reason to change
class UserService:
"""Responsible ONLY for user business logic."""
def __init__(self, email_service: "EmailService", logger: "AppLogger") -> None:
self._email_service = email_service
self._logger = logger
def create_user(self, name: str, email: str) -> dict:
user = {"name": name, "email": email}
# Delegates email and logging to dedicated services
self._email_service.send_welcome(email)
self._logger.log(f"Created user {name}")
return user
class EmailService:
"""Responsible ONLY for sending emails."""
def send_welcome(self, email: str) -> None:
print(f" 📧 Sending welcome email to {email}")
class AppLogger:
"""Responsible ONLY for logging."""
def log(self, message: str) -> None:
print(f" 📝 LOG: {message}")
class ReportService:
"""Responsible ONLY for generating reports."""
def generate_user_report(self) -> str:
return " 📊 User Report: ..."
# ════════════════════════════════════════════════════════════
# O — OPEN/CLOSED PRINCIPLE (OCP)
# ════════════════════════════════════════════════════════════
# ❌ BAD: Adding a new discount type requires modifying existing code
def calculate_discount_bad(customer_type: str, amount: float) -> float:
"""Every new customer type = another elif branch = modification."""
if customer_type == "gold":
return amount * 0.10
elif customer_type == "silver":
return amount * 0.05
elif customer_type == "platinum": # ← had to MODIFY this function
return amount * 0.15
else:
return 0.0
# ✅ GOOD: New discount types = new classes, zero modification to existing code
class DiscountStrategy(ABC):
"""Abstract interface — closed for modification."""
@abstractmethod
def calculate(self, amount: float) -> float: ...
class GoldDiscount(DiscountStrategy):
def calculate(self, amount: float) -> float:
return amount * 0.10
class SilverDiscount(DiscountStrategy):
def calculate(self, amount: float) -> float:
return amount * 0.05
class PlatinumDiscount(DiscountStrategy):
"""New type — added WITHOUT touching any existing class."""
def calculate(self, amount: float) -> float:
return amount * 0.15
def calculate_discount_good(strategy: DiscountStrategy, amount: float) -> float:
"""Open for extension (new strategies), closed for modification."""
return strategy.calculate(amount)
# ════════════════════════════════════════════════════════════
# L — LISKOV SUBSTITUTION PRINCIPLE (LSP)
# ════════════════════════════════════════════════════════════
# ❌ BAD: Square breaks Rectangle's contract
class RectangleBad:
def __init__(self, width: int, height: int) -> None:
self._width = width
self._height = height
@property
def width(self) -> int:
return self._width
@width.setter
def width(self, value: int) -> None:
self._width = value
@property
def height(self) -> int:
return self._height
@height.setter
def height(self, value: int) -> None:
self._height = value
def area(self) -> int:
return self._width * self._height
class SquareBad(RectangleBad):
"""❌ Violates LSP: setting width also sets height (surprise!)."""
@RectangleBad.width.setter
def width(self, value: int) -> None:
self._width = value
self._height = value # ← SURPRISE! Breaks Rectangle's expected behavior
@RectangleBad.height.setter
def height(self, value: int) -> None:
self._width = value # ← SURPRISE!
self._height = value
# ✅ GOOD: Separate abstractions that don't violate each other's contracts
@runtime_checkable
class Shape(Protocol):
"""Both Rectangle and Square implement Shape without pretending to be each other."""
def area(self) -> int: ...
@dataclass
class Rectangle:
width: int
height: int
def area(self) -> int:
return self.width * self.height
@dataclass
class Square:
side: int
def area(self) -> int:
return self.side * self.side
def print_area(shape: Shape) -> None:
"""Works with ANY Shape — no surprises, no broken contracts."""
print(f" 📐 Area = {shape.area()}")
# ════════════════════════════════════════════════════════════
# I — INTERFACE SEGREGATION PRINCIPLE (ISP)
# ════════════════════════════════════════════════════════════
# ❌ BAD: Fat interface forces irrelevant method implementations
class IWorkerBad(ABC):
@abstractmethod
def work(self) -> str: ...
@abstractmethod
def eat(self) -> str: ...
@abstractmethod
def sleep(self) -> str: ...
class RobotBad(IWorkerBad):
def work(self) -> str:
return "Working..."
def eat(self) -> str:
raise NotImplementedError("Robots don't eat!") # ← Forced to implement nonsense
def sleep(self) -> str:
raise NotImplementedError("Robots don't sleep!") # ← Forced to implement nonsense
# ✅ GOOD: Lean interfaces — each client depends only on what it needs
class Workable(Protocol):
def work(self) -> str: ...
class Feedable(Protocol):
def eat(self) -> str: ...
class Sleepable(Protocol):
def sleep(self) -> str: ...
class HumanWorker:
"""Implements all three — because humans work, eat, AND sleep."""
def work(self) -> str:
return "Human working"
def eat(self) -> str:
return "Human eating"
def sleep(self) -> str:
return "Human sleeping"
class RobotWorker:
"""Implements ONLY Workable — no nonsensical eat()/sleep()."""
def work(self) -> str:
return "Robot working"
def assign_task(worker: Workable) -> str:
"""Only cares about work() — doesn't force eat() or sleep()."""
return worker.work()
# ════════════════════════════════════════════════════════════
# D — DEPENDENCY INVERSION PRINCIPLE (DIP)
# ════════════════════════════════════════════════════════════
# ❌ BAD: High-level module directly depends on low-level module
class MySQLDatabase:
def save(self, data: dict) -> None:
print(f" 💾 Saving to MySQL: {data}")
class OrderServiceBad:
def __init__(self) -> None:
self._db = MySQLDatabase() # ← Hardcoded dependency on MySQL
def place_order(self, order: dict) -> None:
self._db.save(order)
# ✅ GOOD: Both depend on an abstraction (Protocol)
class Database(Protocol):
"""The abstraction both high-level and low-level modules depend on."""
def save(self, data: dict) -> None: ...
class MySQLDB:
def save(self, data: dict) -> None:
print(f" 💾 Saving to MySQL: {data}")
class PostgresDB:
def save(self, data: dict) -> None:
print(f" 💾 Saving to PostgreSQL: {data}")
class InMemoryDB:
"""Perfect for testing — swap in without changing OrderService."""
def __init__(self) -> None:
self.store: list[dict] = []
def save(self, data: dict) -> None:
self.store.append(data)
print(f" 💾 Saving to InMemory: {data}")
class OrderService:
"""Depends on Database PROTOCOL, not a specific implementation."""
def __init__(self, db: Database) -> None:
self._db = db # ← Injected abstraction
def place_order(self, order: dict) -> None:
self._db.save(order)
# ════════════════════════════════════════════════════════════
# DEMO
# ════════════════════════════════════════════════════════════
if __name__ == "__main__":
print("=" * 60)
print("S — SINGLE RESPONSIBILITY PRINCIPLE")
print("=" * 60)
email_svc = EmailService()
logger = AppLogger()
user_svc = UserService(email_svc, logger)
user = user_svc.create_user("Alice", "alice@example.com")
print(f" Created: {user}")
print()
print("=" * 60)
print("O — OPEN/CLOSED PRINCIPLE")
print("=" * 60)
amount = 100.0
for strategy in [GoldDiscount(), SilverDiscount(), PlatinumDiscount()]:
discount = calculate_discount_good(strategy, amount)
print(f" {strategy.__class__.__name__}: ${amount} → discount ${discount:.2f}")
print()
print("=" * 60)
print("L — LISKOV SUBSTITUTION PRINCIPLE")
print("=" * 60)
shapes: list[Shape] = [Rectangle(5, 10), Square(7)]
for s in shapes:
print(f" {s.__class__.__name__}:", end="")
print_area(s)
print()
print("=" * 60)
print("I — INTERFACE SEGREGATION PRINCIPLE")
print("=" * 60)
human = HumanWorker()
robot = RobotWorker()
print(f" {assign_task(human)}")
print(f" {assign_task(robot)}")
print()
print("=" * 60)
print("D — DEPENDENCY INVERSION PRINCIPLE")
print("=" * 60)
# Swap database implementation without changing OrderService
for db in [MySQLDB(), PostgresDB(), InMemoryDB()]:
order_svc = OrderService(db)
order_svc.place_order({"item": "Widget", "qty": 3})
print()
print("✅ All SOLID principles demonstrated successfully!")
Pattern 01: Singleton (Creational)
🧒 The 5-Year-Old Explanation
There's only ONE principal in your school. Every teacher, every student — when they say "the principal," they all mean the same person. You can't hire a second principal. If someone new walks in and asks for the principal, they get the same one everyone else talks to.
That's a Singleton: one instance, shared by everyone.
📦 Prerequisites
You should be comfortable with:
- Python classes,
init,new - Class variables vs instance variables
- What a metaclass is (we'll use one for thread-safe Singleton)
- Threading basics (
threading.Lock) - The Borg/Monostate alternative pattern
🔍 How to Spot This Pattern (Signal Detection)
You should think of Singleton when you see:
- "There must be exactly one instance of X in the system"
- Global configuration / settings object
- Connection pool, thread pool, or cache manager
- Logger that must be consistent across all modules
- Hardware interface (one printer spooler, one GPU manager)
- "single instance," "global access," "shared state," "centralized," "one and only"
- You need multiple instances with different configs → use Factory Method (Ch 02)
- You want to pass dependencies explicitly → use Dependency Injection (Ch 25)
- You just need shared data, not a shared object → use a module-level variable
- Testing requires isolated instances → Singleton makes mocking painful
⚠️ Warning: Singleton is the most overused and misused design pattern. In many cases, a simple module-level instance or dependency injection is cleaner. Know when NOT to use it.
🧩 The Core Idea (Technical Deep-Dive)
Intent (GoF)
Ensure a class has only one instance, and provide a global point of access to it.
UML Class Diagram
UML Sequence Diagram
Step-by-Step Explanation
- The class stores a class-level reference (
_instance) to the single instance - The constructor (or
_new) checks ifinstancealready exists - First call:
_instanceisNone→ create the object, store it, return it - All subsequent calls:
_instanceexists → return the existing one - Every caller in the entire application gets the same object
🔧 Pattern Anatomy
1. Participants
| Participant | Python 3.14 Type | Role | Responsibility |
|---|---|---|---|
Singleton |
class with overridden _new_ or metaclass |
The controlled class | Ensures only one instance exists |
_instance |
cls (class variable) |
Instance store | Holds the single instance |
_lock |
threading.Lock |
Thread guard | Prevents race conditions during creation |
2. Collaboration Flow
- Client calls
Singleton()orSingleton.get_instance() _newcheckscls.instance- If
None: acquire lock → double-check → create → store → release lock → return - If exists: return
cls._instance
3. Invariants / Contracts
- Invariant 1:
Singleton() is Singleton()→ alwaysTrue - Invariant 2:
id(instancea) == id(instanceb)for any two references - Invariant 3: Thread-safe version must hold lock only during first creation
📊 Visual Walkthrough
Python Implementation Approaches
Thread-Safety Decision Tree
🔄 Without Pattern → With Pattern Evolution
Level 1: Naive (Global Variables)
# BAD: Nothing prevents creating multiple instances
config1 = DatabaseConfig("postgres://localhost/db1")
config2 = DatabaseConfig("postgres://localhost/db2")
# Now which one is "the" config? Chaos.
- Problem: No control over instantiation, no guarantee of uniqueness
- Code smell: Multiple instances of what should be a shared resource
Level 2: Module-Level Instance (Often Good Enough!)
# config.py
class _DatabaseConfig:
def __init__(self):
self.host = "localhost"
self.port = 5432
# THE singleton — Python modules are only imported once
db_config = _DatabaseConfig()
- What improved: Single instance guaranteed by Python's import system
- Remaining issue: Can still call
_DatabaseConfig()directly; no enforcement
Level 3: Full Singleton Pattern
class DatabaseConfig:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.host = "localhost"
self.port = 5432
self._initialized = True
- Key insight:
_newcontrols object creation BEFOREinit_runs
⏱️ Trade-offs & Complexity
| Aspect | Assessment |
|---|---|
| Added Complexity | Low (simple _new_ override) to Medium (metaclass/thread-safe) |
| SOLID Principle Embodied | None directly — actually risks violating SRP (class + lifecycle control) |
| When Justified | True "one and only one" requirement: DB connection pools, hardware drivers, app config |
| When Over-engineered | When a module-level instance would suffice (90% of cases in Python) |
| Simpler Alternative | Module-level instance (config = Config() at module scope) |
🏗️ Template / Boilerplate (Python 3.14)
Approach 1: _new_ Override (Simple)
class Singleton:
_instance: "Singleton | None" = None
def __new__(cls) -> "Singleton":
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Approach 2: Thread-Safe with Double-Checked Locking
import threading
class ThreadSafeSingleton:
_instance: "ThreadSafeSingleton | None" = None
_lock: threading.Lock = threading.Lock()
def __new__(cls) -> "ThreadSafeSingleton":
if cls._instance is None: # Fast path (no lock)
with cls._lock: # Slow path (lock)
if cls._instance is None: # Double-check
cls._instance = super().__new__(cls)
return cls._instance
Approach 3: Metaclass (Reusable)
class SingletonMeta(type):
_instances: dict[type, object] = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class MyService(metaclass=SingletonMeta):
pass # Automatically a singleton!
🌍 Real-World Framework Sightings
Django
- Where:
django.conf.settings— the global settings object - How:
LazySettingswraps a singleSettingsinstance, created on first access - Also:
django.db.connections— database connection handler is a singleton-like object
Python Standard Library
- Where:
logging.getLogger(name)— returns the SAME logger for the same name - How: Internal registry (
logging.Manager) acts as a flyweight/singleton hybrid - Also:
None,True,False— literal singletons enforced by the interpreter
SQLAlchemy
- Where:
createengine()withpoolsize— engine typically created once - How: While not strictly Singleton, the pattern of "create once, share everywhere" is identical
🔗 Pattern Connections
⚠️ Common Mistakes & Pitfalls
- Mistake: Forgetting that
_initis called EVERY TIME, even whennewreturns existing instance → Fix: Use aninitializedflag to skip re-initialization
- Mistake: Not making it thread-safe in multi-threaded apps → Fix: Use double-checked locking or a metaclass with a lock
- Mistake: Using Singleton when Dependency Injection would be cleaner → Fix: Ask "do I NEED to enforce one instance, or do I just want to SHARE one instance?" If the latter, use DI
- Mistake: Singleton makes unit testing painful (global state persists between tests) → Fix: Add a
_reset()classmethod for test teardown, or use DI instead
- Mistake: Subclassing a Singleton — the subclass and parent share
_instance→ Fix: Use a metaclass that stores instances per-class in a dict
🎤 Interview Communication Guide
Opening statement template:
"This calls for a Singleton because we need to guarantee exactly one instance of [resource] across the entire application, with a global access point."System Design connections:
- Database connection pool manager
- Configuration service in microservices
- Cache manager (Redis client wrapper)
- Logger in distributed systems
- "Why not just use a global variable?" → "A global variable doesn't prevent creating multiple instances. Singleton enforces the constraint."
- "How do you handle thread safety?" → "Double-checked locking: first check without lock (fast path), then lock + second check (safe path)."
- "What about testing?" → "Singleton can make testing harder. I'd consider Dependency Injection if testability is a priority."
- "Is Singleton an anti-pattern?" → "It CAN be. It introduces global state, which couples modules. In Python, a module-level instance is often cleaner. I'd use Singleton only when enforcement of 'exactly one' is critical."
- ✅ You know the pattern AND its drawbacks
- ✅ You can discuss thread-safety unprompted
- ✅ You mention DI as the modern alternative
- ✅ You know Python-specific approaches (
new, metaclass, module-level)
🧪 Self-Test Questions
- What is the difference between
_newandinitin Python, and why does Singleton overridenew_? - Why is double-checked locking needed for thread safety? Why not just always use a lock?
- In Python, modules are only imported once. How does this make modules "natural singletons"?
- What happens if you subclass a
_new_-based Singleton? Does the subclass get its own instance? - (Trick) You need 3 different database connections in your app (read replica, write primary, analytics). Should you use Singleton?
- What is the Borg/Monostate pattern and how does it differ from Singleton?
Answers
_newcreates the object (controls identity),initinitializes it (sets attributes). Singleton overridesnew_because we need to control WHETHER a new object is created, not just how it's initialized.
- Always locking adds unnecessary overhead — after the first creation, every call pays the lock cost. Double-checked locking only acquires the lock when
_instanceisNone(first call), then the outer check short-circuits all subsequent calls.
- When you
import config, Python executesconfig.pyonce and caches the module insys.modules. All subsequent imports get the cached module. Soconfig.db = Database()at module level creates exactly oneDatabaseinstance.
- No! The subclass and parent share
cls.instanceif using a simplenew_override. The first one created wins. Use a metaclass with a per-class dict to fix this.
- No! Singleton guarantees ONE instance. You need THREE. Use Factory Method (Ch 02) or Dependency Injection (Ch 25) to create and manage multiple configured instances.
- Borg shares state (same
_dict_) but creates different objects (id()differs). Singleton shares the object itself (sameid()). Borg is useful when identity doesn't matter but shared state does.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Classic Singleton | Pure Python | _new override, initialized flag |
| 2 | Thread-Safe Singleton | Pure Python | Double-checked locking with threading.Lock |
| 3 | Django Settings | Django | Real-world LazySettings singleton in action |
| 4 | Borg / Monostate | Pure Python | Shared state alternative to Singleton |
⚡ Interactive Python 3.14 Implementations (4)
"""
============================================================
Example: Classic Singleton with __new__ Override
Pattern: Singleton (Creational)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
An application needs a single AppConfig object that loads settings
from environment variables ONCE and is shared across all modules.
Creating multiple AppConfig instances would waste memory and risk
inconsistent state.
PATTERN PARTICIPANTS IN THIS EXAMPLE:
--------------------------------------
- Singleton → AppConfig (overrides __new__)
- _instance → class-level reference to the single object
KEY INSIGHT:
------------
__new__ controls object CREATION (before __init__). By overriding
it, we intercept the construction process and return the existing
instance instead of creating a new one.
============================================================
"""
import os
class AppConfig:
"""
Application configuration — guaranteed single instance.
Uses __new__ to intercept object creation. The _initialized
flag prevents __init__ from re-running on subsequent calls.
"""
_instance: "AppConfig | None" = None
def __new__(cls) -> "AppConfig":
# __new__ is called BEFORE __init__ — this is where we
# control whether a new object is actually created
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self) -> None:
# __init__ is called EVERY time AppConfig() is invoked,
# even when __new__ returns the existing instance.
# The _initialized flag prevents re-initialization.
if self._initialized:
return
# Expensive initialization — runs only ONCE
self.debug: bool = os.getenv("DEBUG", "false").lower() == "true"
self.db_host: str = os.getenv("DB_HOST", "localhost")
self.db_port: int = int(os.getenv("DB_PORT", "5432"))
self.app_name: str = "MyApplication"
self._initialized = True
print(f" ⚙️ AppConfig initialized (debug={self.debug}, db={self.db_host}:{self.db_port})")
@classmethod
def _reset(cls) -> None:
"""For testing only — destroys the singleton to allow re-creation."""
cls._instance = None
def __repr__(self) -> str:
return f"AppConfig(app={self.app_name}, db={self.db_host}:{self.db_port})"
# ─── Usage / Demo ──────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("CLASSIC SINGLETON — __new__ Override")
print("=" * 60)
# First call → creates the instance
config1 = AppConfig()
print(f" config1: {config1}")
print(f" id(config1): {id(config1)}")
print()
# Second call → returns the SAME instance (no re-init message)
config2 = AppConfig()
print(f" config2: {config2}")
print(f" id(config2): {id(config2)}")
print()
# Proof: they are the SAME object
print(f" config1 is config2: {config1 is config2}")
assert config1 is config2, "Singleton contract violated!"
# Mutating one affects the other (same object)
config1.app_name = "ModifiedApp"
assert config2.app_name == "ModifiedApp", "Shared state broken!"
print(f" After mutation: config2.app_name = '{config2.app_name}'")
print()
# Reset for testing
AppConfig._reset()
config3 = AppConfig()
print(f" After reset: config3 is config1? {config3 is config1}")
assert config3 is not config1, "Reset should create a new instance!"
print()
print("✅ Classic Singleton demonstrated successfully!")
"""
============================================================
Example: Thread-Safe Singleton with Double-Checked Locking
Pattern: Singleton (Creational)
Framework: Pure Python (threading)
============================================================
REAL-WORLD SCENARIO:
--------------------
A connection pool manager in a multi-threaded web server. Multiple
request threads may try to create the pool simultaneously on startup.
Without thread safety, two pools could be created — wasting resources
and causing subtle bugs.
PATTERN PARTICIPANTS IN THIS EXAMPLE:
--------------------------------------
- Singleton → ConnectionPool (thread-safe __new__)
- _lock → threading.Lock (guards first creation)
- _instance → the single pool object
KEY INSIGHT:
------------
Double-checked locking: the outer check avoids the lock overhead on
every call (fast path), while the inner check prevents race conditions
during first creation (safe path).
============================================================
"""
import threading
import time
class ConnectionPool:
"""
Thread-safe Singleton connection pool.
Uses double-checked locking to minimize lock overhead:
- Outer check: fast path — no lock acquired if instance exists
- Inner check: safe path — prevents two threads from both creating
"""
_instance: "ConnectionPool | None" = None
_lock: threading.Lock = threading.Lock()
def __new__(cls, pool_size: int = 5) -> "ConnectionPool":
if cls._instance is None: # Fast path (no lock)
with cls._lock: # Acquire lock
if cls._instance is None: # Double-check (safe path)
instance = super().__new__(cls)
instance._initialized = False
cls._instance = instance
return cls._instance
def __init__(self, pool_size: int = 5) -> None:
if self._initialized:
return
# Simulate expensive pool creation
self.pool_size = pool_size
self.connections: list[str] = [
f"conn_{i}" for i in range(pool_size)
]
self._initialized = True
print(f" 🔗 ConnectionPool created with {pool_size} connections (thread: {threading.current_thread().name})")
def get_connection(self) -> str:
"""Get a connection from the pool."""
if self.connections:
return self.connections.pop()
raise RuntimeError("No connections available!")
def return_connection(self, conn: str) -> None:
"""Return a connection to the pool."""
self.connections.append(conn)
@classmethod
def _reset(cls) -> None:
"""For testing only."""
cls._instance = None
def __repr__(self) -> str:
return f"ConnectionPool(size={self.pool_size}, available={len(self.connections)})"
def worker(worker_id: int, results: dict[int, int]) -> None:
"""Simulate a worker thread accessing the singleton."""
pool = ConnectionPool(pool_size=10)
results[worker_id] = id(pool)
# Small sleep to increase chance of race condition in naive impl
time.sleep(0.01)
# ─── Usage / Demo ──────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("THREAD-SAFE SINGLETON — Double-Checked Locking")
print("=" * 60)
# Launch 10 threads simultaneously — all should get the SAME instance
results: dict[int, int] = {}
threads = [
threading.Thread(target=worker, args=(i, results), name=f"Worker-{i}")
for i in range(10)
]
print("\n Launching 10 threads simultaneously...")
for t in threads:
t.start()
for t in threads:
t.join()
# Verify ALL threads got the same instance
unique_ids = set(results.values())
print(f"\n Thread results (object IDs): {list(results.values())[:5]}...")
print(f" Unique instances created: {len(unique_ids)}")
assert len(unique_ids) == 1, f"Thread safety violated! Got {len(unique_ids)} instances"
print(f" All 10 threads share the same instance ✅")
# Verify the pool works
pool = ConnectionPool()
print(f"\n Pool state: {pool}")
conn = pool.get_connection()
print(f" Got connection: {conn}")
pool.return_connection(conn)
print(f" Returned: {conn}")
print("\n✅ Thread-Safe Singleton demonstrated successfully!")
"""
============================================================
Example: Singleton via Metaclass (Reusable)
Pattern: Singleton (Creational)
Framework: Pure Python (simulating Django-style settings)
============================================================
REAL-WORLD SCENARIO:
--------------------
Django's settings module exposes a single LazySettings object that
wraps the project's settings. Here we simulate the same concept:
a Settings class that loads configuration lazily (on first access)
and is guaranteed to be a singleton via a reusable metaclass.
PATTERN PARTICIPANTS IN THIS EXAMPLE:
--------------------------------------
- SingletonMeta → metaclass that enforces single-instance per class
- Settings → the singleton class (analogous to Django's LazySettings)
- CacheManager → another singleton using the SAME metaclass
KEY INSIGHT:
------------
A metaclass-based Singleton is REUSABLE — any class can become a
singleton by simply declaring `metaclass=SingletonMeta`. No code
duplication. The metaclass stores instances in a per-class dict,
so subclasses get their own instances (fixing the inheritance bug).
============================================================
"""
import threading
class SingletonMeta(type):
"""
Metaclass that makes any class a thread-safe Singleton.
Usage:
class MyService(metaclass=SingletonMeta):
pass
How it works:
- type.__call__ is what runs when you write MyClass()
- We override __call__ to check if an instance already exists
- Stores instances in a per-class dict, so subclasses work correctly
"""
_instances: dict[type, object] = {}
_lock: threading.Lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
def _reset(cls) -> None:
"""Remove the stored instance (for testing)."""
cls._instances.pop(cls, None)
# ─── Django-Style Settings Singleton ───────────────
class Settings(metaclass=SingletonMeta):
"""
Simulates Django's settings object.
In Django: django.conf.settings is a LazySettings singleton.
Here we demonstrate the same concept with a metaclass.
"""
def __init__(self) -> None:
# This runs only ONCE (metaclass prevents re-creation)
self.DEBUG: bool = True
self.DATABASE: dict = {
"ENGINE": "django.db.backends.postgresql",
"HOST": "localhost",
"PORT": 5432,
"NAME": "myapp_db",
}
self.SECRET_KEY: str = "super-secret-key-do-not-share"
self.INSTALLED_APPS: list[str] = [
"django.contrib.auth",
"django.contrib.admin",
"myapp.core",
]
print(f" ⚙️ Settings loaded (DEBUG={self.DEBUG})")
def __repr__(self) -> str:
return f"Settings(DEBUG={self.DEBUG}, DB={self.DATABASE['NAME']})"
class CacheManager(metaclass=SingletonMeta):
"""
Another singleton using the SAME metaclass.
Demonstrates reusability — no code duplication!
"""
def __init__(self, backend: str = "redis") -> None:
self.backend = backend
self._cache: dict[str, object] = {}
print(f" 💾 CacheManager initialized (backend={backend})")
def set(self, key: str, value: object) -> None:
self._cache[key] = value
def get(self, key: str) -> object | None:
return self._cache.get(key)
def __repr__(self) -> str:
return f"CacheManager(backend={self.backend}, entries={len(self._cache)})"
# ─── Usage / Demo ──────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("METACLASS SINGLETON — Reusable Across Classes")
print("=" * 60)
# Settings singleton
print("\n--- Settings ---")
s1 = Settings()
s2 = Settings()
print(f" s1: {s1}")
print(f" s2: {s2}")
print(f" s1 is s2: {s1 is s2}")
assert s1 is s2, "Settings singleton violated!"
# CacheManager singleton (SAME metaclass, DIFFERENT instance)
print("\n--- CacheManager ---")
c1 = CacheManager(backend="redis")
c2 = CacheManager(backend="memcached") # Ignored — already created
print(f" c1: {c1}")
print(f" c2: {c2}")
print(f" c1 is c2: {c1 is c2}")
print(f" c2.backend: '{c2.backend}' (still 'redis' — init ran only once)")
assert c1 is c2, "CacheManager singleton violated!"
# Settings and CacheManager are DIFFERENT singletons
print("\n--- Cross-Class Independence ---")
print(f" Settings is CacheManager? {s1 is c1}")
assert s1 is not c1, "Different classes should have different instances!"
# Cache actually works
c1.set("user:123", {"name": "Alice"})
result = c2.get("user:123")
print(f" Cache set via c1, get via c2: {result}")
assert result == {"name": "Alice"}, "Shared state broken!"
print("\n✅ Metaclass Singleton demonstrated successfully!")
"""
============================================================
Example: Borg / Monostate Pattern (Singleton Alternative)
Pattern: Singleton (Creational) — Variant
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
A logging configuration where multiple Logger instances are fine
(different identity / id()), but they MUST share the same state
(same log level, same handlers, same formatter). Borg achieves
this by sharing __dict__ across all instances.
PATTERN PARTICIPANTS IN THIS EXAMPLE:
--------------------------------------
- Borg base class → shares __dict__ across all instances
- LoggerConfig → concrete Borg that shares logging state
KEY INSIGHT:
------------
Singleton: same IDENTITY (same object, same id())
Borg: same STATE (different objects, shared __dict__)
Borg is more Pythonic and subclass-friendly. It also plays nicer
with pickling and copying since instances are "real" objects.
============================================================
"""
from typing import Any
class Borg:
"""
The Borg pattern (a.k.a. Monostate).
All instances share the SAME __dict__, so they have the same
state even though they are different objects (different id()).
Named after the Star Trek Borg: "We are one. Resistance is futile."
"""
_shared_state: dict[str, Any] = {}
def __init__(self) -> None:
# Point this instance's __dict__ to the shared state
# This is the magic — all instances now share attributes
self.__dict__ = self._shared_state
class LoggerConfig(Borg):
"""
Shared logging configuration using Borg pattern.
Every LoggerConfig instance sees the same log_level, handlers,
and formatter — because they all share __dict__.
"""
def __init__(
self,
log_level: str = "INFO",
formatter: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
) -> None:
super().__init__()
# Only initialize if not already set (first instance)
if not hasattr(self, "_initialized"):
self.log_level = log_level
self.formatter = formatter
self.handlers: list[str] = ["console"]
self._initialized = True
print(f" 📋 LoggerConfig initialized (level={log_level})")
def add_handler(self, handler: str) -> None:
if handler not in self.handlers:
self.handlers.append(handler)
print(f" ➕ Added handler: {handler}")
def set_level(self, level: str) -> None:
self.log_level = level
print(f" 🔧 Log level changed to: {level}")
def __repr__(self) -> str:
return f"LoggerConfig(level={self.log_level}, handlers={self.handlers})"
# ─── Usage / Demo ──────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("BORG / MONOSTATE PATTERN")
print("=" * 60)
# Create first instance
logger1 = LoggerConfig(log_level="DEBUG")
print(f" logger1: {logger1}")
print(f" id(logger1): {id(logger1)}")
print()
# Create second instance — shares state with first
logger2 = LoggerConfig()
print(f" logger2: {logger2}")
print(f" id(logger2): {id(logger2)}")
print()
# Key difference from Singleton: DIFFERENT objects, SAME state
print(f" logger1 is logger2: {logger1 is logger2}") # False!
print(f" logger1.__dict__ is logger2.__dict__: {logger1.__dict__ is logger2.__dict__}") # True!
assert logger1 is not logger2, "Borg creates different objects"
assert logger1.__dict__ is logger2.__dict__, "Borg shares __dict__"
print()
# Mutating one affects the other (shared state)
logger1.add_handler("file")
logger1.set_level("WARNING")
print(f"\n After logger1 mutations:")
print(f" logger2.log_level: {logger2.log_level}")
print(f" logger2.handlers: {logger2.handlers}")
assert logger2.log_level == "WARNING", "Shared state broken!"
assert "file" in logger2.handlers, "Shared state broken!"
# Comparison table
print()
print(" ╔════════════════════╦═══════════════════════════╗")
print(" ║ ║ Singleton vs Borg ║")
print(" ╠════════════════════╬═══════════════════════════╣")
print(" ║ Identity (is) ║ Same obj vs Diff objs ║")
print(" ║ State (__dict__) ║ Same vs Same ║")
print(f" ║ id(a) == id(b) ║ True vs {id(logger1) == id(logger2):<14}║")
print(" ║ Subclass-friendly ║ Tricky vs Easy ║")
print(" ║ Pickle/Copy ║ Tricky vs Works ║")
print(" ╚════════════════════╩═══════════════════════════╝")
print("\n✅ Borg / Monostate pattern demonstrated successfully!")
Pattern 02: Factory Method (Creational)
🧒 The 5-Year-Old Explanation
Imagine a toy factory with different machines. You press a button labeled "car" → a toy car pops out. Press "doll" → a toy doll pops out. You don't build the toy yourself — you just tell the factory WHAT you want, and it figures out HOW to make it.
The Factory Method is that button: you ask for a product by type, and the factory creates the right one for you.
📦 Prerequisites
You should be comfortable with:
- Python ABC,
@abstractmethod - Polymorphism — calling the same method on different objects
Protocolfor structural typing- SOLID: Open/Closed Principle (this pattern is OCP incarnate)
🔍 How to Spot This Pattern (Signal Detection)
You should think of Factory Method when you see:
- "Create objects without specifying the exact class"
- Multiple similar classes that share an interface
- Object creation logic needs to be centralized
- The type of object depends on input / configuration / environment
- "parser," "serializer," "handler," "connector," "transport," "renderer"
- "based on type," "depending on format," "plugin system"
- Only one product type → just use the constructor directly
- Creation logic is trivial (no branching needed) → YAGNI
- You need families of related objects → use Abstract Factory (Ch 03)
🧩 The Core Idea (Technical Deep-Dive)
Intent (GoF)
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
1. Participants
| Participant | Python 3.14 Type | Role | Responsibility |
|---|---|---|---|
Creator |
ABC |
Abstract factory | Declares factory_method() |
ConcreteCreator |
Subclass of Creator |
Specific factory | Implements factory_method() to return a specific product |
Product |
Protocol or ABC |
Product interface | Defines the product contract |
ConcreteProduct |
Class | Specific product | Implements the product interface |
2. Collaboration Flow
- Client calls
Creator.operation() operation()callsself.factory_method()internally- The subclass's
factory_method()creates and returns the right product operation()uses the product through the commonProductinterface
3. Invariants / Contracts
factory_method()always returns an object implementing theProductinterface- Client code depends ONLY on
CreatorandProductabstractions - Adding new product types requires ZERO changes to existing code (OCP)
📊 Visual Walkthrough
Factory Method vs Direct Construction
Pythonic Variations
🔄 Without Pattern → With Pattern Evolution
Level 1: Naive — if/elif chain
# BAD: Adding a new format requires modifying this function
def parse(data: str, fmt: str):
if fmt == "json":
return json.loads(data)
elif fmt == "xml":
return xml.parse(data)
elif fmt == "csv": # ← Every new format = another elif
return csv.parse(data)
Level 2: Simple Factory Function
# BETTER: Centralized creation, but still hardcoded types
def create_parser(fmt: str) -> Parser:
parsers = {"json": JSONParser, "xml": XMLParser, "csv": CSVParser}
return parsers[fmt]()
Level 3: Registry-Based Factory (Optimal)
# BEST: Open for extension — register new parsers without touching factory
class ParserFactory:
_registry: dict[str, type[Parser]] = {}
@classmethod
def register(cls, fmt: str, parser: type[Parser]):
cls._registry[fmt] = parser
@classmethod
def create(cls, fmt: str) -> Parser:
return cls._registry[fmt]()
# New parser? Just register it. Zero changes to factory.
ParserFactory.register("yaml", YAMLParser)
⏱️ Trade-offs & Complexity
| Aspect | Assessment |
|---|---|
| Added Complexity | Low — a function or method that returns objects |
| SOLID Principle Embodied | Open/Closed (new types = new classes, not modifications) + Dependency Inversion (client depends on interface) |
| When Justified | Multiple product types behind a common interface; type determined at runtime |
| When Over-engineered | Only one product type; creation logic is trivial |
| Simpler Alternative | Direct constructor call: product = ConcreteProduct() |
🏗️ Template / Boilerplate (Python 3.14)
from abc import ABC, abstractmethod
from typing import Protocol
class Product(Protocol):
def operation(self) -> str: ...
class ConcreteProductA:
def operation(self) -> str:
return "Product A"
class ConcreteProductB:
def operation(self) -> str:
return "Product B"
# Approach 1: Simple factory function
def create_product(product_type: str) -> Product:
registry: dict[str, type] = {
"a": ConcreteProductA,
"b": ConcreteProductB,
}
if product_type not in registry:
raise ValueError(f"Unknown type: {product_type}")
return registry[product_type]()
# Approach 2: ABC-based creator
class Creator(ABC):
@abstractmethod
def factory_method(self) -> Product: ...
def operation(self) -> str:
product = self.factory_method()
return product.operation()
🌍 Real-World Framework Sightings
Django
- Where:
django.forms.Fieldsubclasses — each field type creates its own widget - How:
Field.widget_classis a factory method pattern — each field decides its widget - Also:
django.db.backends— each database backend creates its own connection
FastAPI
- Where:
Depends()— dependency injection creates the right dependency at runtime - How: The dependency function IS the factory; FastAPI calls it and injects the result
SQLAlchemy
- Where:
create_engine("postgresql://...")— the connection string determines which dialect/driver to instantiate - How: Registry of dialects maps URL schemes to engine implementations
🔗 Pattern Connections
⚠️ Common Mistakes & Pitfalls
- Mistake: Returning different INTERFACES from the factory (some products have methods others don't) → Fix: All products must implement the same
Productinterface
- Mistake: Using Factory Method when there's only one product → Fix: Just call the constructor. YAGNI
- Mistake: Making the factory an instance when it should be a classmethod or static function → Fix: In Python, a simple function is usually the best "factory"
- Mistake: Forgetting to handle unknown types (KeyError) → Fix: Explicit error:
raise ValueError(f"Unknown type: {t}")
🎤 Interview Communication Guide
Opening statement template:
"I'd use a Factory Method here because we need to create objects of different types behind a common interface, and the type is determined by [input/config]. This keeps the client code decoupled from the concrete classes."System Design connections:
- Serializer/deserializer selection (JSON, XML, Protobuf)
- Payment processor creation (Stripe, PayPal, Square)
- Notification channel creation (Email, SMS, Push)
- Database driver instantiation (Postgres, MySQL, SQLite)
- ✅ You use OCP reasoning ("adding new types shouldn't modify existing code")
- ✅ You know the Pythonic way (dict registry, not deep class hierarchies)
- ✅ You distinguish Factory Method from Abstract Factory
🧪 Self-Test Questions
- What SOLID principle does Factory Method embody most directly?
- In Python, what's the simplest form of a factory? (Hint: not a class)
- How does Factory Method differ from Abstract Factory?
- What is the "registry pattern" and why is it the Pythonic way to do factories?
- (Trick) You need to create a
Userobject from form data. Should you use Factory Method?
Answers
- Open/Closed Principle: new product types = new classes, no modification to existing factory code.
- A function that returns different objects based on input. Python doesn't need the ceremony of creator/product class hierarchies.
- Factory Method creates one product. Abstract Factory creates families of related products (e.g., a "Modern" factory creates modern chairs AND modern tables).
- A dict mapping type names to classes:
{"json": JSONParser, "xml": XMLParser}. The factory doesregistry[type](). Adding new types is justregistry["yaml"] = YAMLParser— zero code changes.
- No. There's only one product type (
User). Just useUser(**formdata)or a classmethod:User.fromform(data). Factory Method is for when you have MULTIPLE product types.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Basic Factory | Pure Python | Simple factory function + registry |
| 2 | Serializer Factory | Pure Python | Multi-format serialization |
| 3 | FastAPI Dependency Factory | FastAPI-style | DI as factory method |
⚡ Interactive Python 3.14 Implementations (3)
"""
============================================================
Example: Basic Factory with Registry Pattern
Pattern: Factory Method (Creational)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
A document processing system that needs to parse files in multiple
formats (JSON, XML, CSV, YAML). The format is determined at runtime
from the file extension. Adding new formats should require zero
changes to existing code.
PATTERN PARTICIPANTS IN THIS EXAMPLE:
--------------------------------------
- Product → Parser (Protocol)
- ConcreteProducts → JSONParser, XMLParser, CSVParser
- Factory → ParserFactory (registry-based)
KEY INSIGHT:
------------
The registry pattern (dict mapping) is the most Pythonic factory.
New parsers self-register via decorator — zero changes to the factory.
============================================================
"""
from typing import Protocol, Any
import json
import csv
import io
# ─── Product Interface ─────────────────────────────
class Parser(Protocol):
"""All parsers must implement this interface."""
def parse(self, raw_data: str) -> Any: ...
def file_extensions(self) -> list[str]: ...
# ─── Concrete Products ─────────────────────────────
class JSONParser:
"""Parses JSON strings into Python dicts/lists."""
def parse(self, raw_data: str) -> Any:
return json.loads(raw_data)
def file_extensions(self) -> list[str]:
return [".json"]
class CSVParser:
"""Parses CSV strings into list of dicts."""
def parse(self, raw_data: str) -> list[dict]:
reader = csv.DictReader(io.StringIO(raw_data))
return list(reader)
def file_extensions(self) -> list[str]:
return [".csv"]
class XMLParser:
"""Simple XML tag extractor (lightweight, no external deps)."""
def parse(self, raw_data: str) -> dict[str, str]:
# Minimal XML parsing using string operations
# In production you'd use xml.etree.ElementTree
import re
tags = re.findall(r"<(\w+)>(.*?)</\1>", raw_data)
return dict(tags)
def file_extensions(self) -> list[str]:
return [".xml"]
# ─── Factory (Registry-Based) ──────────────────────
class ParserFactory:
"""
Registry-based factory.
Parsers register themselves by format name. The factory creates
the right parser based on the requested format. Adding new
formats requires ZERO changes to this class (OCP).
"""
_registry: dict[str, type] = {}
@classmethod
def register(cls, fmt: str, parser_class: type) -> None:
"""Register a parser class for a format name."""
cls._registry[fmt.lower()] = parser_class
@classmethod
def create(cls, fmt: str) -> Parser:
"""Create a parser instance for the given format."""
fmt_lower = fmt.lower().lstrip(".")
if fmt_lower not in cls._registry:
available = ", ".join(sorted(cls._registry.keys()))
raise ValueError(
f"Unknown format: '{fmt}'. Available: {available}"
)
return cls._registry[fmt_lower]()
@classmethod
def available_formats(cls) -> list[str]:
"""List all registered formats."""
return sorted(cls._registry.keys())
# Register built-in parsers
ParserFactory.register("json", JSONParser)
ParserFactory.register("csv", CSVParser)
ParserFactory.register("xml", XMLParser)
# ─── Adding a NEW format (zero changes to factory!) ─
class INIParser:
"""Parses simple INI-style key=value strings."""
def parse(self, raw_data: str) -> dict[str, str]:
result = {}
for line in raw_data.strip().splitlines():
line = line.strip()
if "=" in line and not line.startswith("#"):
key, value = line.split("=", 1)
result[key.strip()] = value.strip()
return result
def file_extensions(self) -> list[str]:
return [".ini", ".cfg"]
# Just register — factory code untouched!
ParserFactory.register("ini", INIParser)
# ─── Usage / Demo ──────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("FACTORY METHOD — Registry-Based Parser Factory")
print("=" * 60)
# Test data for each format
test_data = {
"json": '{"name": "Alice", "age": 30, "city": "NYC"}',
"csv": "name,age,city\nAlice,30,NYC\nBob,25,LA",
"xml": "<name>Alice</name><age>30</age><city>NYC</city>",
"ini": "name = Alice\nage = 30\ncity = NYC",
}
print(f"\n Available formats: {ParserFactory.available_formats()}")
for fmt, data in test_data.items():
parser = ParserFactory.create(fmt)
result = parser.parse(data)
print(f"\n [{fmt.upper()}] {parser.__class__.__name__}")
print(f" Input: {data[:50]}...")
print(f" Output: {result}")
# Error handling for unknown format
print()
try:
ParserFactory.create("yaml")
except ValueError as e:
print(f" ⚠️ Expected error: {e}")
# Verify OCP: we added INI without touching ParserFactory
assert "ini" in ParserFactory.available_formats()
ini_parser = ParserFactory.create("ini")
result = ini_parser.parse("host = localhost\nport = 5432")
assert result == {"host": "localhost", "port": "5432"}
print("\n✅ Factory Method demonstrated successfully!")
"""
============================================================
Example: Serializer Factory — Multi-Format Serialization
Pattern: Factory Method (Creational)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
An API needs to serialize response data in different formats
(JSON, XML, YAML-like) based on the Accept header. Each format
has its own serializer, but the API handler shouldn't know about
the concrete serializer classes.
PATTERN PARTICIPANTS IN THIS EXAMPLE:
--------------------------------------
- Product → Serializer (ABC)
- ConcreteProducts → JSONSerializer, XMLSerializer, PlainTextSerializer
- Creator → SerializerFactory (classmethod-based)
KEY INSIGHT:
------------
The factory method here is a @classmethod alternative constructor
pattern — very Pythonic. `Serializer.for_content_type("application/json")`
reads like natural English.
============================================================
"""
from abc import ABC, abstractmethod
from typing import Any
# ─── Product (Abstract) ────────────────────────────
class Serializer(ABC):
"""Abstract serializer — all formats implement this."""
@abstractmethod
def serialize(self, data: dict[str, Any]) -> str:
"""Convert a dict to a formatted string."""
...
@abstractmethod
def content_type(self) -> str:
"""Return the MIME content type."""
...
# ─── Factory Method (classmethod) ──────────────
@classmethod
def for_content_type(cls, content_type: str) -> "Serializer":
"""
Factory method: create the right serializer for a content type.
This is the Pythonic 'alternative constructor' pattern.
"""
registry: dict[str, type[Serializer]] = {
"application/json": JSONSerializer,
"text/xml": XMLSerializer,
"application/xml": XMLSerializer,
"text/plain": PlainTextSerializer,
}
normalized = content_type.lower().strip()
if normalized not in registry:
raise ValueError(
f"Unsupported content type: '{content_type}'. "
f"Supported: {list(registry.keys())}"
)
return registry[normalized]()
# ─── Concrete Products ─────────────────────────────
class JSONSerializer(Serializer):
"""Serializes data as JSON."""
def serialize(self, data: dict[str, Any]) -> str:
import json
return json.dumps(data, indent=2)
def content_type(self) -> str:
return "application/json"
class XMLSerializer(Serializer):
"""Serializes data as simple XML."""
def serialize(self, data: dict[str, Any]) -> str:
lines = ["<response>"]
for key, value in data.items():
lines.append(f" <{key}>{value}</{key}>")
lines.append("</response>")
return "\n".join(lines)
def content_type(self) -> str:
return "text/xml"
class PlainTextSerializer(Serializer):
"""Serializes data as key=value plain text."""
def serialize(self, data: dict[str, Any]) -> str:
return "\n".join(f"{k}: {v}" for k, v in data.items())
def content_type(self) -> str:
return "text/plain"
# ─── Simulated API Handler ─────────────────────────
def handle_request(data: dict[str, Any], accept_header: str) -> str:
"""
Simulated API endpoint.
The handler doesn't know about JSONSerializer, XMLSerializer, etc.
It only talks to the Serializer interface through the factory method.
"""
serializer = Serializer.for_content_type(accept_header)
body = serializer.serialize(data)
return f"Content-Type: {serializer.content_type()}\n\n{body}"
# ─── Usage / Demo ──────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("FACTORY METHOD — Serializer Factory")
print("=" * 60)
sample_data = {
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"active": True,
}
# Simulate different clients requesting different formats
accept_headers = [
"application/json",
"text/xml",
"text/plain",
]
for accept in accept_headers:
print(f"\n --- Request: Accept: {accept} ---")
response = handle_request(sample_data, accept)
print(f"{response}")
# Error handling
print("\n --- Request: Accept: text/html ---")
try:
handle_request(sample_data, "text/html")
except ValueError as e:
print(f" ⚠️ {e}")
# Verify polymorphism: handler never imports concrete classes
s1 = Serializer.for_content_type("application/json")
s2 = Serializer.for_content_type("text/xml")
assert isinstance(s1, JSONSerializer)
assert isinstance(s2, XMLSerializer)
assert s1.content_type() != s2.content_type()
print("\n✅ Serializer Factory demonstrated successfully!")
"""
============================================================
Example: FastAPI-Style Dependency Injection as Factory
Pattern: Factory Method (Creational)
Framework: FastAPI-inspired (pure Python simulation)
============================================================
REAL-WORLD SCENARIO:
--------------------
In FastAPI, `Depends()` is essentially a factory method — you
declare WHAT dependency you need, and the framework creates it
for you. This example simulates how FastAPI's DI works under
the hood, showing that Depends() IS the Factory Method pattern.
PATTERN PARTICIPANTS IN THIS EXAMPLE:
--------------------------------------
- Product → Database session, AuthService, etc.
- Factory → Dependency functions (get_db, get_auth_service)
- Creator → The DI framework that calls the factories
KEY INSIGHT:
------------
FastAPI's Depends() is Factory Method in disguise. The dependency
function IS the factory — it creates and yields the right resource.
The endpoint never knows HOW the resource was created.
============================================================
"""
from dataclasses import dataclass, field
from typing import Any, Callable, Generator
from contextlib import contextmanager
# ─── Simulated Database ────────────────────────────
@dataclass
class DatabaseSession:
"""Simulates a database session (like SQLAlchemy Session)."""
db_url: str
_queries: list[str] = field(default_factory=list)
_committed: bool = False
def execute(self, query: str) -> str:
self._queries.append(query)
return f"Result of: {query}"
def commit(self) -> None:
self._committed = True
def close(self) -> None:
pass # Cleanup
def __repr__(self) -> str:
return f"DBSession(url={self.db_url}, queries={len(self._queries)})"
# ─── Dependency Factories (the "factory methods") ──
def get_db() -> Generator[DatabaseSession, None, None]:
"""
Factory that creates a database session.
This IS the Factory Method — FastAPI calls this function,
gets a session, injects it into the endpoint, and cleans
up after the request.
"""
db = DatabaseSession(db_url="postgresql://localhost/myapp")
try:
yield db
db.commit()
finally:
db.close()
@dataclass
class AuthService:
"""Simulates an authentication service."""
secret_key: str = "my-jwt-secret"
def verify_token(self, token: str) -> dict[str, Any]:
# Simplified — in reality, decode JWT
if token == "valid-token":
return {"user_id": 42, "role": "admin"}
raise PermissionError("Invalid token")
def get_auth_service() -> AuthService:
"""Factory for AuthService — no cleanup needed, so no generator."""
return AuthService(secret_key="production-secret-key")
@dataclass
class CurrentUser:
user_id: int
role: str
def get_current_user(
token: str = "valid-token",
auth: AuthService | None = None,
) -> CurrentUser:
"""
Factory that DEPENDS ON another factory (chained DI).
In FastAPI: Depends(get_current_user) which itself uses
Depends(get_auth_service). Factories composing factories!
"""
if auth is None:
auth = get_auth_service()
payload = auth.verify_token(token)
return CurrentUser(user_id=payload["user_id"], role=payload["role"])
# ─── Simulated DI Container ────────────────────────
class DependencyInjector:
"""
Simplified version of how FastAPI's Depends() works internally.
This IS the Factory Method pattern: the injector calls factory
functions and provides their results to the endpoint.
"""
def __init__(self) -> None:
self._overrides: dict[Callable, Callable] = {}
def override(self, original: Callable, replacement: Callable) -> None:
"""Override a dependency (for testing)."""
self._overrides[original] = replacement
def resolve(self, factory: Callable) -> Any:
"""
Resolve a dependency by calling its factory.
If overridden (for testing), call the override instead.
"""
actual_factory = self._overrides.get(factory, factory)
result = actual_factory()
# Handle generators (like get_db which yields)
if hasattr(result, "__next__"):
return next(result)
return result
# ─── Simulated FastAPI Endpoints ───────────────────
def list_users_endpoint(db: DatabaseSession, user: CurrentUser) -> dict:
"""
Simulated endpoint.
In FastAPI this would be:
@app.get("/users")
def list_users(db: Session = Depends(get_db),
user: CurrentUser = Depends(get_current_user)):
The endpoint doesn't know HOW db or user were created.
It just USES them. That's Factory Method + DI.
"""
if user.role != "admin":
return {"error": "Forbidden"}
result = db.execute("SELECT * FROM users")
return {"data": result, "requested_by": user.user_id}
# ─── Usage / Demo ──────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("FACTORY METHOD — FastAPI-Style Dependency Injection")
print("=" * 60)
injector = DependencyInjector()
# Resolve dependencies using factories
print("\n --- Production Dependencies ---")
db = injector.resolve(get_db)
print(f" DB: {db}")
auth = injector.resolve(get_auth_service)
print(f" Auth: {auth}")
user = injector.resolve(get_current_user)
print(f" User: {user}")
# Call endpoint with injected dependencies
response = list_users_endpoint(db=db, user=user)
print(f" Response: {response}")
# ── Testing: Override factories ──────────────────
print("\n --- Test Dependencies (Overridden) ---")
# Create test doubles
def get_test_db() -> DatabaseSession:
return DatabaseSession(db_url="sqlite:///:memory:")
def get_test_user() -> CurrentUser:
return CurrentUser(user_id=999, role="tester")
injector.override(get_db, get_test_db)
injector.override(get_current_user, get_test_user)
test_db = injector.resolve(get_db)
test_user = injector.resolve(get_current_user)
print(f" Test DB: {test_db}")
print(f" Test User: {test_user}")
assert test_db.db_url == "sqlite:///:memory:", "Override should use test DB"
assert test_user.role == "tester", "Override should use test user"
print("\n Key insight: Depends() IS Factory Method!")
print(" The dependency function = the factory")
print(" The framework = the client that calls the factory")
print(" Override for testing = swap the factory, not the product")
print("\n✅ FastAPI-Style Dependency Factory demonstrated successfully!")
Pattern 03: Abstract Factory (Creational)
🧒 The 5-Year-Old Explanation
Imagine a furniture catalog. You pick a STYLE: "Modern" or "Victorian." Once you pick the style, everything matches — the chair, the table, the sofa are ALL in that style. You don't mix a modern chair with a Victorian table. The catalog (abstract factory) ensures every piece comes from the same family.
📦 Prerequisites
You should be comfortable with:
- Factory Method (Ch 02) — Abstract Factory uses factory methods internally
- ABC +
@abstractmethodfor abstract factories and products - SOLID: Dependency Inversion (depend on abstract factory, not concrete)
🔍 How to Spot This Pattern
Think of Abstract Factory when you see:
- Families of related objects that must be used together
- Cross-platform UI (Windows/Mac widgets), multi-database support
- Theme/skin systems where all elements must be consistent
- "Swap the entire set" requirements
- Only one product type → Factory Method (Ch 02)
- Products are unrelated → separate factories, not an abstract factory
🧩 The Core Idea
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
AbstractFactory |
Protocol / ABC |
Declares creation methods for each product |
ConcreteFactory |
Class | Creates products from ONE family |
AbstractProduct |
Protocol / ABC |
Interface for a type of product |
ConcreteProduct |
Class | Specific product in a family |
Client |
Function/class | Uses factories and products through abstract interfaces only |
🔄 Without Pattern → With Pattern Evolution
Level 1: Hardcoded platform checks everywhere
# BAD: Platform checks scattered across entire codebase
if platform == "mac":
button = MacButton()
checkbox = MacCheckbox()
# ... 50 more places
Level 3: Abstract Factory
# GOOD: One decision point, consistent family
factory = get_factory(platform) # returns MacFactory or WinFactory
button = factory.create_button() # Always gets matching product
checkbox = factory.create_checkbox()
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Medium — more classes than Factory Method |
| SOLID | DIP (client depends on abstract factory) + OCP (new families = new factories) |
| When Justified | Multiple product types that MUST be used together in matching families |
| When Over-engineered | Only one product type (use Factory Method) or families never change |
🌍 Real-World Framework Sightings
SQLAlchemy
- Where:
createengine("postgresql://...")vscreateengine("sqlite://...") - How: Each dialect is an abstract factory producing matching connections, compilers, and type objects
Django
- Where:
django.db.backends— each database backend is a factory for connections, cursors, operations, and schema editors
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | GUI Toolkit Factory | Pure Python | Cross-platform UI widgets |
| 2 | Database Connector Factory | SQLAlchemy-style | Multi-DB abstraction |
⚡ Interactive Python 3.14 Implementations (2)
"""
============================================================
Example: Cross-Platform GUI Toolkit Factory
Pattern: Abstract Factory (Creational)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
A cross-platform desktop app needs to render native-looking UI
widgets (buttons, checkboxes, text inputs) for Windows, macOS,
and Linux. Each platform has its own look and feel, but all widgets
must come from the SAME platform — no mixing Mac buttons with
Windows checkboxes.
KEY INSIGHT:
------------
Abstract Factory guarantees family consistency. Swap the factory,
and ALL products change together. The client code never knows
which platform it's running on.
============================================================
"""
from abc import ABC, abstractmethod
from typing import Protocol
import sys
# ─── Abstract Products ─────────────────────────────
class Button(Protocol):
def render(self) -> str: ...
def on_click(self, handler: str) -> str: ...
class Checkbox(Protocol):
def render(self) -> str: ...
def toggle(self) -> str: ...
class TextInput(Protocol):
def render(self) -> str: ...
def set_value(self, text: str) -> str: ...
# ─── Abstract Factory ──────────────────────────────
class GUIFactory(ABC):
"""Abstract factory — declares creation methods for each widget type."""
@abstractmethod
def create_button(self, label: str) -> Button: ...
@abstractmethod
def create_checkbox(self, label: str) -> Checkbox: ...
@abstractmethod
def create_text_input(self, placeholder: str) -> TextInput: ...
# ─── macOS Family ───────────────────────────────────
class MacButton:
def __init__(self, label: str) -> None:
self.label = label
def render(self) -> str:
return f" 🍎 [ {self.label} ] (rounded macOS button)"
def on_click(self, handler: str) -> str:
return f" 🍎 Button '{self.label}' → {handler}"
class MacCheckbox:
def __init__(self, label: str) -> None:
self.label = label
self.checked = False
def render(self) -> str:
icon = "☑" if self.checked else "☐"
return f" 🍎 {icon} {self.label} (macOS checkbox)"
def toggle(self) -> str:
self.checked = not self.checked
return self.render()
class MacTextInput:
def __init__(self, placeholder: str) -> None:
self.placeholder = placeholder
self.value = ""
def render(self) -> str:
display = self.value or self.placeholder
return f" 🍎 ╭─────────────────╮\n │ {display:<15} │ (macOS input)\n ╰─────────────────╯"
def set_value(self, text: str) -> str:
self.value = text
return self.render()
class MacFactory(GUIFactory):
def create_button(self, label: str) -> Button:
return MacButton(label)
def create_checkbox(self, label: str) -> Checkbox:
return MacCheckbox(label)
def create_text_input(self, placeholder: str) -> TextInput:
return MacTextInput(placeholder)
# ─── Windows Family ────────────────────────────────
class WinButton:
def __init__(self, label: str) -> None:
self.label = label
def render(self) -> str:
return f" 🪟 [ {self.label} ] (flat Windows button)"
def on_click(self, handler: str) -> str:
return f" 🪟 Button '{self.label}' → {handler}"
class WinCheckbox:
def __init__(self, label: str) -> None:
self.label = label
self.checked = False
def render(self) -> str:
icon = "[x]" if self.checked else "[ ]"
return f" 🪟 {icon} {self.label} (Windows checkbox)"
def toggle(self) -> str:
self.checked = not self.checked
return self.render()
class WinTextInput:
def __init__(self, placeholder: str) -> None:
self.placeholder = placeholder
self.value = ""
def render(self) -> str:
display = self.value or self.placeholder
return f" 🪟 |{display:<17}| (Windows input)"
def set_value(self, text: str) -> str:
self.value = text
return self.render()
class WindowsFactory(GUIFactory):
def create_button(self, label: str) -> Button:
return WinButton(label)
def create_checkbox(self, label: str) -> Checkbox:
return WinCheckbox(label)
def create_text_input(self, placeholder: str) -> TextInput:
return WinTextInput(placeholder)
# ─── Factory Selector ──────────────────────────────
def get_factory(platform: str = "") -> GUIFactory:
"""Select the right factory based on platform."""
if not platform:
platform = sys.platform
factories: dict[str, type[GUIFactory]] = {
"darwin": MacFactory,
"mac": MacFactory,
"win32": WindowsFactory,
"windows": WindowsFactory,
}
factory_class = factories.get(platform, MacFactory)
return factory_class()
# ─── Application (Client) ──────────────────────────
def render_login_form(factory: GUIFactory) -> None:
"""
The application code doesn't know which platform's widgets
it's using. It only talks to the abstract factory + products.
"""
print(" ── Login Form ──")
username = factory.create_text_input("Enter username")
password = factory.create_text_input("Enter password")
remember = factory.create_checkbox("Remember me")
submit = factory.create_button("Sign In")
print(username.render())
print(password.render())
print(remember.render())
print(submit.render())
# ─── Demo ──────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("ABSTRACT FACTORY — Cross-Platform GUI Toolkit")
print("=" * 60)
for platform in ["mac", "windows"]:
print(f"\n ═══ Platform: {platform.upper()} ═══")
factory = get_factory(platform)
render_login_form(factory)
# Key point: the SAME render_login_form() code works with
# BOTH factories — zero platform-specific code in the client
print()
assert isinstance(get_factory("mac"), MacFactory)
assert isinstance(get_factory("windows"), WindowsFactory)
print("✅ Abstract Factory demonstrated successfully!")
"""
============================================================
Example: Database Connector Abstract Factory
Pattern: Abstract Factory (Creational)
Framework: SQLAlchemy-inspired
============================================================
REAL-WORLD SCENARIO:
--------------------
An ORM needs to support multiple databases (PostgreSQL, SQLite).
Each database requires matching components: connection, cursor,
and query compiler. Mixing a PostgreSQL connection with a SQLite
compiler would be catastrophic. Abstract Factory ensures all
components come from the same database family.
KEY INSIGHT:
------------
SQLAlchemy's dialect system IS an Abstract Factory. create_engine()
selects the right dialect, which then produces matching connections,
type objects, and SQL compilers.
============================================================
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
# ─── Abstract Products ─────────────────────────────
class Connection(ABC):
@abstractmethod
def connect(self) -> str: ...
@abstractmethod
def close(self) -> str: ...
class Cursor(ABC):
@abstractmethod
def execute(self, query: str) -> str: ...
class QueryCompiler(ABC):
@abstractmethod
def compile_select(self, table: str, columns: list[str]) -> str: ...
@abstractmethod
def compile_insert(self, table: str, data: dict) -> str: ...
# ─── PostgreSQL Family ─────────────────────────────
class PostgresConnection(Connection):
def connect(self) -> str:
return " 🐘 Connected to PostgreSQL on port 5432"
def close(self) -> str:
return " 🐘 PostgreSQL connection closed"
class PostgresCursor(Cursor):
def execute(self, query: str) -> str:
return f" 🐘 PG executing: {query}"
class PostgresCompiler(QueryCompiler):
def compile_select(self, table: str, columns: list[str]) -> str:
cols = ", ".join(columns)
return f'SELECT {cols} FROM "{table}"' # PG uses double quotes
def compile_insert(self, table: str, data: dict) -> str:
cols = ", ".join(data.keys())
vals = ", ".join(f"'{v}'" for v in data.values())
return f'INSERT INTO "{table}" ({cols}) VALUES ({vals}) RETURNING id'
# ─── SQLite Family ──────────────────────────────────
class SQLiteConnection(Connection):
def connect(self) -> str:
return " 💾 Connected to SQLite (file: ./app.db)"
def close(self) -> str:
return " 💾 SQLite connection closed"
class SQLiteCursor(Cursor):
def execute(self, query: str) -> str:
return f" 💾 SQLite executing: {query}"
class SQLiteCompiler(QueryCompiler):
def compile_select(self, table: str, columns: list[str]) -> str:
cols = ", ".join(columns)
return f"SELECT {cols} FROM [{table}]" # SQLite uses brackets
def compile_insert(self, table: str, data: dict) -> str:
cols = ", ".join(data.keys())
vals = ", ".join(f"'{v}'" for v in data.values())
return f"INSERT INTO [{table}] ({cols}) VALUES ({vals})"
# ─── Abstract Factory ──────────────────────────────
class DatabaseFactory(ABC):
"""Abstract factory for database component families."""
@abstractmethod
def create_connection(self) -> Connection: ...
@abstractmethod
def create_cursor(self) -> Cursor: ...
@abstractmethod
def create_compiler(self) -> QueryCompiler: ...
class PostgresFactory(DatabaseFactory):
def create_connection(self) -> Connection:
return PostgresConnection()
def create_cursor(self) -> Cursor:
return PostgresCursor()
def create_compiler(self) -> QueryCompiler:
return PostgresCompiler()
class SQLiteFactory(DatabaseFactory):
def create_connection(self) -> Connection:
return SQLiteConnection()
def create_cursor(self) -> Cursor:
return SQLiteCursor()
def create_compiler(self) -> QueryCompiler:
return SQLiteCompiler()
# ─── Engine (like SQLAlchemy's create_engine) ──────
def create_engine(url: str) -> DatabaseFactory:
"""
Simulates SQLAlchemy's create_engine().
The URL determines which abstract factory (dialect) to use.
All components produced by that factory are guaranteed to
be compatible with each other.
"""
scheme = url.split("://")[0].lower()
factories: dict[str, type[DatabaseFactory]] = {
"postgresql": PostgresFactory,
"postgres": PostgresFactory,
"sqlite": SQLiteFactory,
}
if scheme not in factories:
raise ValueError(f"Unsupported database: {scheme}")
return factories[scheme]()
# ─── ORM-like Client ───────────────────────────────
def run_queries(factory: DatabaseFactory) -> None:
"""
Client code — doesn't know if it's talking to PG or SQLite.
All components come from the same family (abstract factory).
"""
conn = factory.create_connection()
cursor = factory.create_cursor()
compiler = factory.create_compiler()
print(conn.connect())
# Compile and execute a SELECT
select_sql = compiler.compile_select("users", ["id", "name", "email"])
print(cursor.execute(select_sql))
# Compile and execute an INSERT
insert_sql = compiler.compile_insert("users", {"name": "Alice", "email": "alice@example.com"})
print(cursor.execute(insert_sql))
print(conn.close())
# ─── Demo ──────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("ABSTRACT FACTORY — Database Connector (SQLAlchemy-style)")
print("=" * 60)
for url in ["postgresql://localhost/myapp", "sqlite:///./app.db"]:
print(f"\n ═══ Engine: {url} ═══")
engine = create_engine(url)
run_queries(engine)
# The SAME run_queries() function works with both databases
# because it only depends on the abstract factory interface
assert isinstance(create_engine("postgresql://x"), PostgresFactory)
assert isinstance(create_engine("sqlite:///x"), SQLiteFactory)
print("\n✅ Database Abstract Factory demonstrated successfully!")
Pattern 04: Builder (Creational)
🧒 The 5-Year-Old Explanation
Ordering a custom pizza: First you pick the crust (thin or thick), then the sauce (tomato or white), then toppings one by one (pepperoni, mushrooms, olives). You control EACH step, and at the end you get exactly the pizza you designed. The pizza maker (builder) follows your instructions step by step.
📦 Prerequisites
You should be comfortable with:
- Method chaining (fluent interfaces):
builder.setx().sety().build() @dataclassfor product classes- Optional parameters and default values
🔍 How to Spot This Pattern
Think of Builder when you see:
- Complex object with many optional parameters
- Constructor with 5+ arguments (telescoping constructor anti-pattern)
- Object needs to be constructed step-by-step
- Same construction process should create different representations
- Object has few required params → just use the constructor
- No step-by-step construction → use Factory Method (Ch 02)
🧩 The Core Idea
Intent (GoF)
Separate the construction of a complex object from its representation so the same construction process can create different representations.
UML Class Diagram
UML Sequence Diagram
🔄 Without Pattern → With Pattern
Level 1: Telescoping Constructor
# BAD: 10 parameters, most optional — which is which?
query = Query("users", ["name"], True, False, None, 10, 0, "name", "ASC", None)
Level 3: Builder with Fluent API
# GOOD: Clear, readable, self-documenting
query = (QueryBuilder("users")
.select("name", "email")
.where("active = true")
.order_by("name")
.limit(10)
.build())
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low-Medium — simple for fluent builders |
| SOLID | SRP (builder = construction, product = domain logic) |
| When Justified | Complex objects with many optional parts; step-by-step construction |
| Simpler Alternative | @dataclass with defaults; keyword arguments |
🌍 Real-World Framework Sightings
SQLAlchemy
Query.filter().order_by().limit()— fluent query builder
Django
QuerySet.filter().exclude().order_by()[:10]— chainable query API
Pydantic
modelvalidator+modelconstruct()— step-by-step model building
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | SQL Query Builder | Pure Python | Fluent API with method chaining |
| 2 | HTTP Request Builder | Pure Python | Step-by-step request construction |
⚡ Interactive Python 3.14 Implementations (2)
"""
============================================================
Example: SQL Query Builder with Fluent API
Pattern: Builder (Creational)
Framework: Pure Python (SQLAlchemy-inspired)
============================================================
REAL-WORLD SCENARIO:
--------------------
Building SQL queries programmatically. A query has many optional
parts (WHERE, ORDER BY, LIMIT, JOIN, GROUP BY). Using a builder
with method chaining makes construction readable and prevents
SQL injection through parameterized values.
KEY INSIGHT:
------------
Each method returns `self`, enabling fluent chaining:
QueryBuilder("users").select("name").where("active=true").limit(10).build()
============================================================
"""
from dataclasses import dataclass, field
from typing import Self
@dataclass
class Query:
"""The product — an immutable SQL query."""
sql: str
params: dict[str, object] = field(default_factory=dict)
def __str__(self) -> str:
return f"SQL: {self.sql}\nParams: {self.params}"
class QueryBuilder:
"""
Fluent SQL query builder.
Each method returns `self` (using `Self` type hint from Python 3.11+)
to enable method chaining. The build() method produces the final Query.
"""
def __init__(self, table: str) -> None:
self._table = table
self._columns: list[str] = ["*"]
self._conditions: list[str] = []
self._order: list[str] = []
self._limit: int | None = None
self._offset: int | None = None
self._joins: list[str] = []
self._group_by: list[str] = []
self._params: dict[str, object] = {}
def select(self, *columns: str) -> Self:
"""Set columns to select."""
self._columns = list(columns)
return self
def where(self, condition: str, **params: object) -> Self:
"""Add a WHERE condition with optional named parameters."""
self._conditions.append(condition)
self._params.update(params)
return self
def join(self, table: str, on: str) -> Self:
"""Add a JOIN clause."""
self._joins.append(f"JOIN {table} ON {on}")
return self
def order_by(self, column: str, desc: bool = False) -> Self:
"""Add ORDER BY clause."""
direction = "DESC" if desc else "ASC"
self._order.append(f"{column} {direction}")
return self
def group_by(self, *columns: str) -> Self:
"""Add GROUP BY clause."""
self._group_by = list(columns)
return self
def limit(self, n: int) -> Self:
"""Set LIMIT."""
self._limit = n
return self
def offset(self, n: int) -> Self:
"""Set OFFSET."""
self._offset = n
return self
def build(self) -> Query:
"""Assemble and return the final Query product."""
parts = [f"SELECT {', '.join(self._columns)}"]
parts.append(f"FROM {self._table}")
if self._joins:
parts.extend(self._joins)
if self._conditions:
parts.append(f"WHERE {' AND '.join(self._conditions)}")
if self._group_by:
parts.append(f"GROUP BY {', '.join(self._group_by)}")
if self._order:
parts.append(f"ORDER BY {', '.join(self._order)}")
if self._limit is not None:
parts.append(f"LIMIT {self._limit}")
if self._offset is not None:
parts.append(f"OFFSET {self._offset}")
return Query(sql="\n".join(parts), params=self._params)
# ─── Demo ──────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("BUILDER — SQL Query Builder with Fluent API")
print("=" * 60)
# Simple query
print("\n --- Simple SELECT ---")
q1 = QueryBuilder("users").select("id", "name", "email").build()
print(f" {q1.sql}")
# Complex query with all options
print("\n --- Complex Query ---")
q2 = (
QueryBuilder("orders")
.select("orders.id", "users.name", "orders.total")
.join("users", "users.id = orders.user_id")
.where("orders.status = :status", status="completed")
.where("orders.total > :min_total", min_total=100)
.order_by("orders.total", desc=True)
.limit(20)
.offset(0)
.build()
)
print(f" {q2}")
# Aggregation query
print("\n --- Aggregation ---")
q3 = (
QueryBuilder("products")
.select("category", "COUNT(*) as count", "AVG(price) as avg_price")
.where("active = true")
.group_by("category")
.order_by("count", desc=True)
.limit(5)
.build()
)
print(f" {q3.sql}")
# Verify builder produces correct SQL
assert "SELECT id, name, email" in q1.sql
assert "JOIN users ON" in q2.sql
assert "WHERE" in q2.sql
assert "LIMIT 20" in q2.sql
assert q2.params == {"status": "completed", "min_total": 100}
assert "GROUP BY category" in q3.sql
print("\n✅ Query Builder demonstrated successfully!")
"""
============================================================
Example: HTTP Request Builder
Pattern: Builder (Creational)
Framework: Pure Python (requests-inspired)
============================================================
REAL-WORLD SCENARIO:
--------------------
Building HTTP requests with many optional parts: URL, method,
headers, query parameters, body, timeout, authentication.
A builder makes this readable and prevents argument-order confusion.
KEY INSIGHT:
------------
The builder validates constraints at build() time, not during
individual set calls. This allows flexible construction order
while ensuring the final product is always valid.
============================================================
"""
from dataclasses import dataclass, field
from typing import Any, Self
import json
@dataclass(frozen=True)
class HTTPRequest:
"""Immutable HTTP request product."""
method: str
url: str
headers: dict[str, str]
params: dict[str, str]
body: str | None
timeout: int
auth: tuple[str, str] | None
def __str__(self) -> str:
lines = [f"{self.method} {self.url}"]
if self.params:
qs = "&".join(f"{k}={v}" for k, v in self.params.items())
lines[0] += f"?{qs}"
for k, v in self.headers.items():
lines.append(f" {k}: {v}")
if self.body:
lines.append(f" Body: {self.body[:80]}...")
return "\n".join(lines)
class RequestBuilder:
"""Fluent HTTP request builder."""
def __init__(self, url: str) -> None:
self._url = url
self._method = "GET"
self._headers: dict[str, str] = {}
self._params: dict[str, str] = {}
self._body: str | None = None
self._timeout = 30
self._auth: tuple[str, str] | None = None
def method(self, m: str) -> Self:
self._method = m.upper()
return self
def get(self) -> Self:
return self.method("GET")
def post(self) -> Self:
return self.method("POST")
def put(self) -> Self:
return self.method("PUT")
def delete(self) -> Self:
return self.method("DELETE")
def header(self, key: str, value: str) -> Self:
self._headers[key] = value
return self
def content_type(self, ct: str) -> Self:
return self.header("Content-Type", ct)
def accept(self, ct: str) -> Self:
return self.header("Accept", ct)
def bearer_token(self, token: str) -> Self:
return self.header("Authorization", f"Bearer {token}")
def param(self, key: str, value: str) -> Self:
self._params[key] = value
return self
def json_body(self, data: dict[str, Any]) -> Self:
self._body = json.dumps(data)
self.content_type("application/json")
return self
def timeout(self, seconds: int) -> Self:
self._timeout = seconds
return self
def basic_auth(self, username: str, password: str) -> Self:
self._auth = (username, password)
return self
def build(self) -> HTTPRequest:
# Validation at build time
if not self._url:
raise ValueError("URL is required")
if self._method in ("POST", "PUT", "PATCH") and not self._body:
raise ValueError(f"{self._method} requires a body")
return HTTPRequest(
method=self._method,
url=self._url,
headers=dict(self._headers),
params=dict(self._params),
body=self._body,
timeout=self._timeout,
auth=self._auth,
)
# ─── Demo ──────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("BUILDER — HTTP Request Builder")
print("=" * 60)
# Simple GET request
print("\n --- Simple GET ---")
req1 = (
RequestBuilder("https://api.example.com/users")
.get()
.accept("application/json")
.param("page", "1")
.param("limit", "20")
.timeout(10)
.build()
)
print(f" {req1}")
# Authenticated POST request
print("\n --- Authenticated POST ---")
req2 = (
RequestBuilder("https://api.example.com/orders")
.post()
.bearer_token("eyJ0eXAiOiJKV1QiLCJhbG...")
.json_body({"item": "Widget", "qty": 3, "price": 29.99})
.timeout(30)
.build()
)
print(f" {req2}")
# Validation: POST without body should fail
print("\n --- Validation ---")
try:
RequestBuilder("https://api.example.com/bad").post().build()
except ValueError as e:
print(f" ⚠️ Expected error: {e}")
assert req1.method == "GET"
assert req2.method == "POST"
assert "Bearer" in req2.headers.get("Authorization", "")
assert req1.timeout == 10
print("\n✅ HTTP Request Builder demonstrated successfully!")
Pattern 05: Prototype (Creational)
🧒 The 5-Year-Old Explanation
Using a photocopier: instead of drawing a picture from scratch every time, you put the original in the copier and press "copy." The copy is identical but independent — you can color on the copy without ruining the original. That's Prototype: clone an existing object instead of building one from scratch.
📦 Prerequisites
You should be comfortable with:
- Python
copymodule:copy.copy()(shallow) vscopy.deepcopy()(deep) - Mutable vs immutable objects
copyanddeepcopymagic methods
🔍 How to Spot This Pattern
Think of Prototype when you see:
- Creating objects is expensive (DB queries, network calls, complex initialization)
- You need many similar objects with slight variations
- "Template" objects that serve as starting points
- Dynamic class registration (clone a prototype from a registry)
- Object creation is cheap → just use the constructor
- Objects are immutable → no need to clone, share the same instance
- Each object is completely different → Factory Method is better
🧩 The Core Idea
UML Class Diagram
UML Sequence Diagram
🔄 Shallow vs Deep Copy
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low — Python's copy module does the heavy lifting |
| When Justified | Expensive initialization; many similar objects; template-based creation |
| Simpler Alternative | Direct construction with shared defaults |
🌍 Real-World Framework Sightings
Django
Model.objects.get(pk=1)→ modify →instance.pk = None; instance.save()clones a DB row
Python Standard Library
copy.copy()/copy.deepcopy()— built-in prototype supportlist.copy(),dict.copy()— shallow clone methods
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Document Cloning | Pure Python | Deep copy with customization |
| 2 | Game Object Prototypes | Pure Python | Registry of prototypes |
⚡ Interactive Python 3.14 Implementations (2)
"""
============================================================
Example: Document Cloning with Deep Copy
Pattern: Prototype (Creational)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
A document management system where users create documents from
templates. Instead of building each document from scratch (loading
fonts, styles, headers, footers), clone a template and customize.
KEY INSIGHT:
------------
Deep copy creates a fully independent clone. Modifying the clone
has ZERO effect on the original (or any other clones).
============================================================
"""
import copy
from dataclasses import dataclass, field
@dataclass
class Style:
font: str = "Arial"
size: int = 12
color: str = "#000000"
bold: bool = False
@dataclass
class Document:
"""A document that can be cloned from a template."""
title: str
content: str
author: str
styles: dict[str, Style] = field(default_factory=dict)
tags: list[str] = field(default_factory=list)
metadata: dict[str, str] = field(default_factory=dict)
def clone(self) -> "Document":
"""Prototype method — deep copy to ensure full independence."""
return copy.deepcopy(self)
def __str__(self) -> str:
styles_str = ", ".join(f"{k}:{v.font}/{v.size}" for k, v in self.styles.items())
return (
f"Document('{self.title}' by {self.author}, "
f"tags={self.tags}, styles=[{styles_str}])"
)
# ─── Template Registry ─────────────────────────────
class TemplateRegistry:
"""Registry of document templates (prototypes)."""
_templates: dict[str, Document] = {}
@classmethod
def register(cls, name: str, template: Document) -> None:
cls._templates[name] = template
@classmethod
def create_from(cls, name: str, **overrides: str) -> Document:
"""Clone a template and apply overrides."""
if name not in cls._templates:
raise ValueError(f"Template '{name}' not found")
doc = cls._templates[name].clone()
for key, value in overrides.items():
setattr(doc, key, value)
return doc
# ─── Demo ──────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("PROTOTYPE — Document Cloning")
print("=" * 60)
# Create a rich template (expensive to build from scratch)
report_template = Document(
title="Quarterly Report Template",
content="[Report content placeholder]",
author="Template",
styles={
"heading": Style(font="Helvetica", size=24, bold=True),
"body": Style(font="Georgia", size=11, color="#333333"),
"footer": Style(font="Arial", size=8, color="#999999"),
},
tags=["report", "quarterly"],
metadata={"version": "1.0", "department": "Engineering"},
)
TemplateRegistry.register("report", report_template)
# Clone and customize — much faster than rebuilding from scratch
print("\n --- Cloning from template ---")
q1_report = TemplateRegistry.create_from(
"report", title="Q1 2025 Report", author="Alice"
)
q1_report.tags.append("Q1")
q1_report.metadata["quarter"] = "Q1"
print(f" Clone 1: {q1_report}")
q2_report = TemplateRegistry.create_from(
"report", title="Q2 2025 Report", author="Bob"
)
q2_report.tags.append("Q2")
q2_report.styles["heading"].color = "#0066cc"
print(f" Clone 2: {q2_report}")
# Verify deep independence
print("\n --- Independence check ---")
print(f" Template tags: {report_template.tags}")
print(f" Q1 tags: {q1_report.tags}")
print(f" Q2 tags: {q2_report.tags}")
print(f" Template heading color: {report_template.styles['heading'].color}")
print(f" Q2 heading color: {q2_report.styles['heading'].color}")
assert "Q1" not in report_template.tags, "Template should be unmodified!"
assert "Q2" not in q1_report.tags, "Q1 should be independent of Q2!"
assert report_template.styles["heading"].color == "#000000", "Template style unchanged!"
assert q2_report.styles["heading"].color == "#0066cc", "Clone style changed!"
print("\n✅ Document Cloning demonstrated successfully!")
"""
============================================================
Example: Game Object Prototypes Registry
Pattern: Prototype (Creational)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
A game engine needs to spawn thousands of enemies, items, and
terrain tiles. Each type starts from a prototype (template) with
base stats, then gets randomized variations. Building each from
scratch with full initialization (loading sprites, calculating
stats) is too slow. Clone and customize instead.
KEY INSIGHT:
------------
A prototype registry (like a spawner catalog) lets you register
base objects once, then rapidly clone + customize. This is how
real game engines handle object pooling and spawning.
============================================================
"""
import copy
import random
from dataclasses import dataclass, field
@dataclass
class Stats:
health: int
attack: int
defense: int
speed: float
@dataclass
class GameObject:
name: str
category: str
stats: Stats
abilities: list[str] = field(default_factory=list)
loot_table: dict[str, float] = field(default_factory=dict)
position: tuple[float, float] = (0.0, 0.0)
def clone(self) -> "GameObject":
return copy.deepcopy(self)
def randomize(self, variance: float = 0.2) -> "GameObject":
"""Add random variance to stats (for mob variety)."""
s = self.stats
factor = lambda: 1.0 + random.uniform(-variance, variance)
self.stats = Stats(
health=int(s.health * factor()),
attack=int(s.attack * factor()),
defense=int(s.defense * factor()),
speed=round(s.speed * factor(), 1),
)
return self
def __str__(self) -> str:
s = self.stats
return (
f"{self.name} [{self.category}] "
f"HP:{s.health} ATK:{s.attack} DEF:{s.defense} SPD:{s.speed} "
f"@ {self.position}"
)
class GameObjectSpawner:
"""Prototype registry — stores base prototypes, spawns clones."""
_prototypes: dict[str, GameObject] = {}
@classmethod
def register(cls, key: str, prototype: GameObject) -> None:
cls._prototypes[key] = prototype
@classmethod
def spawn(
cls,
key: str,
position: tuple[float, float] = (0.0, 0.0),
randomize: bool = True,
) -> GameObject:
if key not in cls._prototypes:
raise ValueError(f"Unknown prototype: '{key}'")
obj = cls._prototypes[key].clone()
obj.position = position
if randomize:
obj.randomize()
return obj
@classmethod
def spawn_group(
cls,
key: str,
count: int,
area: tuple[float, float, float, float],
) -> list[GameObject]:
"""Spawn multiple clones in a rectangular area."""
x_min, y_min, x_max, y_max = area
return [
cls.spawn(
key,
position=(
round(random.uniform(x_min, x_max), 1),
round(random.uniform(y_min, y_max), 1),
),
)
for _ in range(count)
]
# ─── Register prototypes ──────────────────────────
GameObjectSpawner.register("goblin", GameObject(
name="Goblin",
category="enemy",
stats=Stats(health=50, attack=10, defense=5, speed=1.2),
abilities=["slash", "dodge"],
loot_table={"gold": 0.8, "potion": 0.3, "dagger": 0.05},
))
GameObjectSpawner.register("dragon", GameObject(
name="Dragon",
category="boss",
stats=Stats(health=500, attack=80, defense=40, speed=0.8),
abilities=["fire_breath", "tail_swipe", "fly"],
loot_table={"gold": 1.0, "legendary_sword": 0.1, "dragon_scale": 0.5},
))
GameObjectSpawner.register("health_potion", GameObject(
name="Health Potion",
category="item",
stats=Stats(health=0, attack=0, defense=0, speed=0),
abilities=["heal_50"],
loot_table={},
))
# ─── Demo ──────────────────────────────────────────
if __name__ == "__main__":
random.seed(42) # For reproducible demo output
print("=" * 60)
print("PROTOTYPE — Game Object Spawner")
print("=" * 60)
# Spawn individual enemies
print("\n --- Single Spawns ---")
g1 = GameObjectSpawner.spawn("goblin", position=(10.0, 20.0))
g2 = GameObjectSpawner.spawn("goblin", position=(15.0, 22.0))
boss = GameObjectSpawner.spawn("dragon", position=(100.0, 100.0))
print(f" {g1}")
print(f" {g2}")
print(f" {boss}")
# Verify independence (different stats due to randomization)
assert g1.stats.health != g2.stats.health or g1.stats.attack != g2.stats.attack, \
"Clones should have randomized stats"
assert g1.position != g2.position, "Clones at different positions"
# Spawn a group of goblins in an area
print("\n --- Group Spawn (5 goblins) ---")
patrol = GameObjectSpawner.spawn_group("goblin", 5, (0, 0, 50, 50))
for g in patrol:
print(f" {g}")
# Verify prototype is unmodified
print("\n --- Prototype integrity check ---")
proto = GameObjectSpawner._prototypes["goblin"]
print(f" Original prototype: {proto}")
assert proto.stats.health == 50, "Prototype should be unchanged!"
assert proto.position == (0.0, 0.0), "Prototype position unchanged!"
# Items don't need randomization
print("\n --- Item spawn (no randomization) ---")
potion = GameObjectSpawner.spawn("health_potion", position=(5, 5), randomize=False)
print(f" {potion}")
print("\n✅ Game Object Spawner demonstrated successfully!")
Pattern 06: Adapter (Structural)
🧒 The 5-Year-Old Explanation
You travel to Europe, and your phone charger has two flat prongs. But the wall socket in Europe has two round holes. You can't plug your charger in! What do you do? You buy a travel adapter. The adapter has round prongs on one side (to fit the wall) and flat holes on the other (to fit your charger).
The Adapter pattern does exactly this for code: it makes two incompatible interfaces work together without changing either of them.
📦 Prerequisites
You should be comfortable with:
- Python's Multiple Inheritance (for Class Adapters)
- Object Composition vs Inheritance (Ch 00)
- Protocols/ABCs
🔍 How to Spot This Pattern
Think of Adapter when you see:
- You want to use an existing class, but its interface doesn't match what you need.
- You're integrating a third-party library or legacy code and want to hide its weird API.
- You have several existing subclasses that lack some common functionality, and you can't just modify the superclass.
- You want to change the behavior of an object, not just its interface → Decorator (Ch 09)
- You want to simplify a complex set of interfaces → Facade (Ch 10)
🧩 The Core Idea
Intent (GoF)
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
UML Class Diagram (Object Adapter - Composition)
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Target |
Protocol or ABC |
The interface the Client expects |
Client |
Class / Function | Collaborates with objects conforming to the Target interface |
Adaptee |
Class | The existing, incompatible interface (often 3rd party or legacy) |
Adapter |
Class | Adapts the Adaptee to the Target interface |
📊 Visual Walkthrough
Object Adapter vs Class Adapter
🔄 Without Pattern → With Pattern
Level 1: Bleeding Legacy Code
# BAD: Client code has to know about the weird legacy API
class ModernSystem:
def process(self, data):
# We want to call save_data(), but we are forced to use LegacyDB
legacy = LegacyDB()
# Have to convert data to XML manually here...
xml_data = dict_to_xml(data)
legacy.insert_record_xml(xml_data)
Level 3: Adapter Pattern
# GOOD: Client only knows the clean interface
class ModernSystem:
def __init__(self, db: ModernDatabaseInterface):
self.db = db
def process(self, data):
self.db.save_data(data) # Clean!
# The adapter hides the ugly XML conversion
class LegacyDBAdapter(ModernDatabaseInterface):
def __init__(self, legacy_db: LegacyDB):
self.legacy_db = legacy_db
def save_data(self, data: dict):
xml_data = dict_to_xml(data)
self.legacy_db.insert_record_xml(xml_data)
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low - just a wrapper class. |
| SOLID | SRP (separates data conversion from primary logic), OCP (new adapters can be added without breaking client code) |
| When Justified | Reusing existing classes with incompatible APIs. |
| When Over-engineered | If you can just modify the original class to match the interface. |
🌍 Real-World Framework Sightings
Python Standard Library
io.TextIOWrapperacts as an adapter, providing a text-oriented interface on top of a buffered binary stream.
Django / SQLAlchemy
- Database backends act as adapters, providing a unified ORM interface over wildly different database driver APIs (e.g.,
psycopg2vssqlite3).
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Object Adapter (Composition) | Pure Python | Adapting a legacy XML API to a modern JSON-expecting client |
| 2 | Class Adapter (Multiple Inheritance) | Pure Python | Adapting via multiple inheritance (Python specific) |
⚡ Interactive Python 3.14 Implementations (2)
"""
============================================================
Example: Object Adapter (Composition)
Pattern: Adapter (Structural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have a modern data analytics application that expects all
incoming data in JSON format. However, you need to integrate
a legacy third-party library that only exports data in XML.
You cannot change the analytics app (it expects a specific interface)
and you cannot change the 3rd party library (it's closed source).
You create an Adapter that wraps the legacy library, translates
the XML to JSON, and passes it to the analytics app.
KEY INSIGHT:
------------
This is the "Object Adapter" pattern, which uses composition.
The adapter holds a reference to the Adaptee (the legacy object).
This is generally preferred over Class Adapters (inheritance)
because it's more flexible and follows "Favor Composition over Inheritance".
============================================================
"""
from typing import Protocol
import json
# ─── Target (The interface the client expects) ─────────
class JSONProvider(Protocol):
"""The modern interface expected by our client."""
def get_json_data(self) -> str: ...
# ─── Client (Our modern application) ───────────────────
class AnalyticsApp:
"""A modern app that only speaks JSON."""
def process_data(self, provider: JSONProvider) -> None:
print(" 📊 AnalyticsApp is requesting data...")
json_data = provider.get_json_data()
# Parse the JSON just to prove it worked
parsed = json.loads(json_data)
print(f" ✅ AnalyticsApp successfully processed: {parsed}")
# ─── Adaptee (The legacy / 3rd party component) ────────
class LegacyXMLAPI:
"""A legacy system that only speaks XML. We cannot modify this class."""
def fetch_xml_records(self) -> str:
print(" 📻 LegacyXMLAPI generating XML...")
return "<person><name>Alice</name><age>30</age></person>"
# ─── Adapter (The bridge between them) ─────────────────
class XMLToJSONAdapter:
"""
Object Adapter using COMPOSITION.
Implements the Target interface (JSONProvider) but routes
the actual work to the wrapped Adaptee (LegacyXMLAPI).
"""
def __init__(self, adaptee: LegacyXMLAPI) -> None:
self._adaptee = adaptee
def get_json_data(self) -> str:
"""Translate the Adaptee's interface into the Target interface."""
print(" 🔌 Adapter intercepting call to get_json_data()...")
# 1. Call the adaptee
xml_data = self._adaptee.fetch_xml_records()
# 2. Translate the data (simulating XML parsing)
print(f" 🔌 Adapter converting XML to JSON: {xml_data}")
# Extremely simplified XML-to-JSON for demonstration purposes
import re
name_match = re.search(r"<name>(.*?)</name>", xml_data)
age_match = re.search(r"<age>(.*?)</age>", xml_data)
data_dict = {
"name": name_match.group(1) if name_match else "Unknown",
"age": int(age_match.group(1)) if age_match else 0
}
# 3. Return what the client expects
return json.dumps(data_dict)
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("OBJECT ADAPTER (Composition)")
print("=" * 60)
# 1. We have our client
app = AnalyticsApp()
# 2. We have the incompatible legacy object
legacy_api = LegacyXMLAPI()
# app.process_data(legacy_api) <-- THIS WOULD CRASH (AttributeError)
# 3. We wrap the legacy object in the adapter
adapter = XMLToJSONAdapter(legacy_api)
# 4. Client happily processes data, unaware of the XML under the hood
app.process_data(adapter)
print("\n✅ Object Adapter demonstrated successfully!")
"""
============================================================
Example: Class Adapter (Multiple Inheritance)
Pattern: Adapter (Structural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have a notification system that expects objects to have a
`send_alert(msg)` method. You have a third-party `TwilioSMS`
class that has a `dispatch_text_message(text)` method.
Instead of wrapping the Twilio object (Object Adapter), you create
a new class that inherits from BOTH the target interface and the
third-party class.
KEY INSIGHT:
------------
This is the "Class Adapter" pattern. It relies on Multiple Inheritance,
which is natively supported in Python. It does not require wrapping an
instance, but it tightly couples the adapter to the specific Adaptee class.
Because it uses inheritance, it cannot adapt subclasses of the Adaptee.
============================================================
"""
from typing import Protocol
# ─── Target (The interface the client expects) ─────────
class AlertSystem(Protocol):
"""The interface our system uses to send alerts."""
def send_alert(self, message: str) -> None: ...
# ─── Client ────────────────────────────────────────────
def notify_admin(alerter: AlertSystem, message: str) -> None:
"""Client function that relies on the AlertSystem interface."""
print(" 🚨 System issue detected! Triggering alert...")
alerter.send_alert(message)
# ─── Adaptee (The 3rd party class) ─────────────────────
class TwilioSMS:
"""Third-party SMS service with an incompatible API."""
def dispatch_text_message(self, text: str) -> None:
print(f" 📱 [Twilio API] Dispatching SMS: '{text}'")
# ─── Adapter (The bridge via Multiple Inheritance) ─────
class TwilioClassAdapter(TwilioSMS):
"""
Class Adapter using MULTIPLE INHERITANCE (implicit Protocol implementation).
Inherits from the Adaptee (TwilioSMS).
Implements the Target interface (`send_alert`).
"""
def send_alert(self, message: str) -> None:
"""Translates the Target interface to the Adaptee interface."""
print(" 🔌 Class Adapter intercepting send_alert()...")
# Because we INHERIT from TwilioSMS, we just call `self`
formatted_msg = f"URGENT: {message}"
self.dispatch_text_message(formatted_msg)
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("CLASS ADAPTER (Multiple Inheritance)")
print("=" * 60)
# We instantiate the ADAPTER directly.
# We don't create a TwilioSMS object and pass it in.
adapter = TwilioClassAdapter()
# The client works with the adapter seamlessly
notify_admin(adapter, "Database connection lost!")
print()
print(" Note: TwilioClassAdapter is a subclass of TwilioSMS.")
print(f" isinstance(adapter, TwilioSMS): {isinstance(adapter, TwilioSMS)}")
print("\n✅ Class Adapter demonstrated successfully!")
Pattern 07: Bridge (Structural)
🧒 The 5-Year-Old Explanation
Think about universal remote controls and TVs. You can have a Samsung TV or an LG TV. You can have a basic remote or a smart remote. If you tried to make a class for every combination, you'd have: SamsungBasicRemote, SamsungSmartRemote, LGBasicRemote, LGSmartRemote. That's too many!
Instead, you build a Bridge. You have one Remote (the "Abstraction") that holds a connection to a TV (the "Implementation"). You can swap the TV or swap the remote independently without creating 100 confusing combinations.
📦 Prerequisites
You should be comfortable with:
- Composition vs Inheritance (Ch 00)
- Interfaces / Protocols
🔍 How to Spot This Pattern
Think of Bridge when you see:
- A class hierarchy growing in two independent dimensions (e.g., Shape/Color, GUI/OS, Remote/Device) causing a Cartesian product explosion of subclasses.
- You want to separate the high-level logic (Abstraction) from the low-level platform details (Implementation) so they can vary independently.
- You need to switch implementations at runtime.
- The two dimensions are tightly coupled and will never vary independently.
- You are trying to make two existing, incompatible interfaces work together → Adapter (Ch 06).
🧩 The Core Idea
Intent (GoF)
Decouple an abstraction from its implementation so that the two can vary independently.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Abstraction |
Class | High-level control layer (e.g., GUI, Remote). Holds a reference to the Implementation. |
RefinedAbstraction |
Subclass | Extends the Abstraction with more features (e.g., SmartRemote). |
Implementation |
Protocol / ABC |
Low-level execution layer interface (e.g., OS API, TV API). |
ConcreteImplementation |
Class | Concrete low-level logic (e.g., Windows API, Samsung TV API). |
📊 Visual Walkthrough
The Subclass Explosion (Inheritance) vs Bridge (Composition)
🔄 Without Pattern → With Pattern
Level 1: Subclass Explosion
# BAD: If we add a new OS (Linux) or a new GUI element (Checkbox),
# we have to create many new classes (LinuxButton, MacCheckbox, etc.)
class MacButton(Button): pass
class WindowsButton(Button): pass
class MacWindow(Window): pass
class WindowsWindow(Window): pass
Level 3: Bridge
# GOOD: GUI elements (Abstraction) and OS APIs (Implementation) vary independently
class GUIElement:
def __init__(self, renderer: OSRenderer):
self.renderer = renderer # The Bridge
class Button(GUIElement):
def click(self):
self.renderer.render_button()
class WindowsRenderer(OSRenderer):
def render_button(self): pass
class MacRenderer(OSRenderer):
def render_button(self): pass
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | High - requires designing two parallel class hierarchies. |
| SOLID | OCP (add new abstractions or implementations independently), SRP (separates high-level logic from low-level platform details). |
| When Justified | Avoiding an exponential explosion of subclasses caused by multiple orthogonal dimensions of variation. |
🌍 Real-World Framework Sightings
Python Standard Library
- The
loggingmodule separates theLogger(Abstraction) from theHandler(Implementation). You can log to a file, stream, or network socket without changing how you calllogger.info().
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Remote and Devices | Pure Python | Decoupling controllers (remotes) from devices (TV/Radio) |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Device & Remote Control Bridge
Pattern: Bridge (Structural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have devices (TV, Radio) and remote controls (Basic, Advanced).
Instead of creating `BasicTVRemote`, `AdvancedTVRemote`, `BasicRadioRemote`,
you separate them into two hierarchies connected by a bridge.
- Abstraction: Remote Control (High-level operations like toggle_power)
- Implementation: Device (Low-level operations like set_volume, enable)
KEY INSIGHT:
------------
You can add a new Device (e.g., SmartSpeaker) without touching ANY Remote code.
You can add a new Remote (e.g., VoiceRemote) without touching ANY Device code.
They vary completely independently.
============================================================
"""
from typing import Protocol
# ─── IMPLEMENTATION HIERARCHY (Low-Level) ───────────────
class Device(Protocol):
"""The Implementation interface."""
def is_enabled(self) -> bool: ...
def enable(self) -> None: ...
def disable(self) -> None: ...
def get_volume(self) -> int: ...
def set_volume(self, percent: int) -> None: ...
def get_channel(self) -> int: ...
def set_channel(self, channel: int) -> None: ...
class TV:
"""Concrete Implementation 1"""
def __init__(self) -> None:
self._on = False
self._volume = 30
self._channel = 1
def is_enabled(self) -> bool:
return self._on
def enable(self) -> None:
self._on = True
def disable(self) -> None:
self._on = False
def get_volume(self) -> int:
return self._volume
def set_volume(self, percent: int) -> None:
self._volume = max(0, min(100, percent))
def get_channel(self) -> int:
return self._channel
def set_channel(self, channel: int) -> None:
self._channel = channel
def __str__(self) -> str:
state = "ON" if self._on else "OFF"
return f"📺 TV is {state} (Vol: {self._volume}, Ch: {self._channel})"
class Radio:
"""Concrete Implementation 2"""
def __init__(self) -> None:
self._on = False
self._volume = 50
self._frequency = 88.1 # MHz instead of channels, but adapted to the interface
def is_enabled(self) -> bool:
return self._on
def enable(self) -> None:
self._on = True
def disable(self) -> None:
self._on = False
def get_volume(self) -> int:
return self._volume
def set_volume(self, percent: int) -> None:
self._volume = max(0, min(100, percent))
def get_channel(self) -> int:
return int(self._frequency * 10)
def set_channel(self, channel: int) -> None:
self._frequency = channel / 10.0
def __str__(self) -> str:
state = "ON" if self._on else "OFF"
return f"📻 Radio is {state} (Vol: {self._volume}, Freq: {self._frequency}MHz)"
# ─── ABSTRACTION HIERARCHY (High-Level) ─────────────────
class RemoteControl:
"""
The Base Abstraction.
It holds a reference to the Implementation (Device) -- THIS IS THE BRIDGE!
"""
def __init__(self, device: Device) -> None:
self.device = device # <--- The Bridge
def toggle_power(self) -> None:
if self.device.is_enabled():
self.device.disable()
else:
self.device.enable()
def volume_down(self) -> None:
self.device.set_volume(self.device.get_volume() - 10)
def volume_up(self) -> None:
self.device.set_volume(self.device.get_volume() + 10)
def channel_down(self) -> None:
self.device.set_channel(self.device.get_channel() - 1)
def channel_up(self) -> None:
self.device.set_channel(self.device.get_channel() + 1)
class AdvancedRemoteControl(RemoteControl):
"""
Refined Abstraction.
Adds new features without touching the Device interface.
"""
def mute(self) -> None:
self.device.set_volume(0)
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("BRIDGE PATTERN")
print("=" * 60)
# 1. Use Basic Remote with TV
tv = TV()
remote = RemoteControl(tv)
print(" [Using Basic Remote with TV]")
remote.toggle_power()
remote.volume_up()
print(f" {tv}")
print("\n [Switching to Advanced Remote for the SAME TV]")
advanced_remote = AdvancedRemoteControl(tv)
advanced_remote.mute()
print(f" {tv}")
# 2. Use Advanced Remote with Radio
print("\n [Using Advanced Remote with Radio]")
radio = Radio()
radio_remote = AdvancedRemoteControl(radio)
radio_remote.toggle_power()
radio_remote.channel_up()
radio_remote.channel_up()
print(f" {radio}")
print("\n✅ Bridge pattern demonstrated successfully!")
Pattern 08: Composite (Structural)
🧒 The 5-Year-Old Explanation
Imagine a box of toys. Inside the box, you have individual toys (like a car or a ball), but you also have smaller boxes inside! If I tell you "move the big box to the car," you move EVERYTHING inside it automatically.
The Composite pattern lets you treat a single object (a toy) and a group of objects (a box of toys) exactly the same way.
📦 Prerequisites
You should be comfortable with:
- Recursion (functions calling themselves)
- Tree data structures (Nodes, Leaves, Branches)
- Protocols/ABCs for defining a common interface
🔍 How to Spot This Pattern
Think of Composite when you see:
- A Part-Whole hierarchy (trees).
- You want clients to ignore the difference between individual objects (Leaves) and compositions of objects (Branches/Composites).
- Files and Directories, UI components (a Panel contains Buttons and other Panels), Menus and Sub-menus.
- The objects don't form a tree structure.
- You don't need to treat groups and individual items uniformly.
🧩 The Core Idea
Intent (GoF)
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Component |
Protocol / ABC |
Declares the interface for objects in the composition. |
Leaf |
Class | Represents leaf objects in the composition. A leaf has no children. Implements the Component interface. |
Composite |
Class | Represents a complex component that may have children. Implements the Component interface and routes calls to its children. |
🔄 Without Pattern → With Pattern
Level 1: Checking Types (The Anti-Pattern)
# BAD: Client has to know if it's dealing with a single item or a list
def get_total_price(item):
if isinstance(item, Box):
total = 0
for child in item.items:
total += get_total_price(child) # Recursion mixed with type checking
return total
elif isinstance(item, Product):
return item.price
Level 3: Composite Pattern
# GOOD: Client just calls the method. It doesn't care if it's a Box or a Product.
def get_total_price(item: Item):
return item.get_price() # Polymorphism and recursion handle it!
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Medium - conceptualizing trees and recursion can be tricky. |
| SOLID | OCP (you can introduce new component types without breaking existing code). Can violate ISP if leaf nodes are forced to implement add()/remove() methods that throw errors (though in modern Python, we often leave those out of the Protocol and use structural typing). |
| When Justified | Dealing with tree structures where groups and individuals should be treated identically. |
🌍 Real-World Framework Sightings
GUI Frameworks (Tkinter, PyQt)
- A
WindowcontainsFrames, which containButtonsandLabels. Callingdraw()orhide()on theWindowcascades down to all children.
DOM (Document Object Model)
- An HTML page is a tree. A (Composite) contains
andtags (Leaves).
✅ Examples in This Chapter
# Example Framework Key Concept 1 File System (Files and Directories) Pure Python Recursively calculating sizes and rendering trees ⚡ Interactive Python 3.14 Implementations (1)
example_01_composite.py File System (Files and Directories)""" ============================================================ Example: File System (Files and Directories) Pattern: Composite (Structural) Framework: Pure Python ============================================================ REAL-WORLD SCENARIO: -------------------- A file system consists of Files (Leaves) and Directories (Composites). A Directory can contain Files AND other Directories. If you ask for the size of a File, it returns its size in bytes. If you ask for the size of a Directory, it recursively asks all its children for their sizes and adds them up. KEY INSIGHT: ------------ The client just calls `get_size()` or `render()`. It does not need to check `if isinstance(child, Directory)`. The polymorphism handles the recursion naturally. ============================================================ """ from typing import Protocol # ─── Component Interface ─────────────────────────────── class FileSystemNode(Protocol): """The Component interface that both Leaves and Composites implement.""" @property def name(self) -> str: ... def get_size(self) -> int: """Returns size in bytes.""" ... def render(self, depth: int = 0) -> str: """Renders the node as a string for tree visualization.""" ... # ─── Leaf ────────────────────────────────────────────── class File: """Leaf node. Represents an actual file with a size. Has no children.""" def __init__(self, name: str, size: int) -> None: self._name = name self._size = size @property def name(self) -> str: return self._name def get_size(self) -> int: return self._size def render(self, depth: int = 0) -> str: indent = " " * depth return f"{indent}📄 {self.name} ({self.get_size()} bytes)" # ─── Composite ───────────────────────────────────────── class Directory: """ Composite node. Contains children (which can be Files or other Directories). Routes calls to its children. """ def __init__(self, name: str) -> None: self._name = name self._children: list[FileSystemNode] = [] @property def name(self) -> str: return self._name def add(self, node: FileSystemNode) -> None: self._children.append(node) def remove(self, node: FileSystemNode) -> None: self._children.remove(node) def get_size(self) -> int: """ Recursively calculates the size of the directory. Notice there is NO type checking (isinstance). It just calls get_size()! """ return sum(child.get_size() for child in self._children) def render(self, depth: int = 0) -> str: """Recursively renders the directory tree.""" indent = " " * depth lines = [f"{indent}📁 {self.name}/ (Total: {self.get_size()} bytes)"] for child in self._children: lines.append(child.render(depth + 1)) return "\n".join(lines) # ─── Demo ────────────────────────────────────────────── if __name__ == "__main__": print("=" * 60) print("COMPOSITE PATTERN - File System") print("=" * 60) # 1. Create Leaves (Files) file1 = File("readme.md", 1024) file2 = File("logo.png", 2048) file3 = File("script.py", 512) file4 = File("secret_key.pem", 256) # 2. Create Composites (Directories) and build the tree root = Directory("root") docs = Directory("docs") docs.add(file1) docs.add(file2) src = Directory("src") src.add(file3) # We can put a directory inside a directory! hidden = Directory(".hidden") hidden.add(file4) src.add(hidden) # Add directories to root root.add(docs) root.add(src) # 3. Client interacts with the tree print("\n [Rendering the entire File System Tree]") print(root.render()) print("\n [Client interacting with individual nodes vs the root]") print(f" Size of {file3.name}: {file3.get_size()} bytes") print(f" Size of {docs.name} directory: {docs.get_size()} bytes") print(f" Size of entire {root.name} directory: {root.get_size()} bytes") print("\n✅ Composite pattern demonstrated successfully!")
Pattern 09: Decorator (Structural)
🧒 The 5-Year-Old Explanation
Imagine you have a plain donut. It's good, but you want more. You add glaze to it. Now it's a glazed donut. Then you add sprinkles. Now it's a glazed donut with sprinkles.
You didn't bake a whole new type of donut from scratch; you just wrapped the original donut in new layers (glaze, then sprinkles).
The Decorator pattern lets you attach new behaviors to an object by putting it inside special wrapper objects that contain those behaviors.
📦 Prerequisites
You should be comfortable with:
- Python
@decoratorsyntax (functions wrapping functions) - Object Composition vs Inheritance
- Protocols / ABCs
🔍 How to Spot This Pattern
Think of Decorator when you see:
- You want to add functionality to an object dynamically, at runtime, without affecting other objects of the same class.
- You have a class explosion because of different combinations of features (e.g.,
Coffee,MilkCoffee,VanillaCoffee,MilkVanillaCoffee). - You see layers or "onion-like" structures where an object is wrapped repeatedly.
- You want to change the interface of the object to match something else → Adapter (Ch 06)
- You want to control access to the object (lazy loading, caching) → Proxy (Ch 12)
🧩 The Core Idea
Intent (GoF)
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Component |
Protocol / ABC |
The interface shared by both the base objects and the decorators. |
ConcreteComponent |
Class | The base object being decorated (the plain donut). |
Decorator |
Class | Maintains a reference to a Component object and defines an interface that conforms to Component's interface. |
ConcreteDecorator |
Class | Adds state or behavior to the component (the glaze, the sprinkles). |
📊 Visual Walkthrough
Inheritance vs Decorator (Subclass Explosion vs Wrapping)
🔄 Without Pattern → With Pattern
Level 1: Subclass Explosion (The Anti-Pattern)
# BAD: We need a new class for every possible combination!
class Text:
def render(self): return "hello"
class BoldText(Text):
def render(self): return f"<b>{super().render()}</b>"
class ItalicText(Text):
def render(self): return f"<i>{super().render()}</i>"
class BoldItalicText(Text): # The explosion begins...
def render(self): return f"<b><i>{super().render()}</i></b>"
Level 3: Decorator Pattern (Object-Oriented)
# GOOD: We can wrap components endlessly!
base = Text("hello")
bold = BoldDecorator(base)
bold_italic = ItalicDecorator(bold)
print(bold_italic.render()) # <i><b>hello</b></i>
🐍 Python's Native Decorators (@) vs GoF Decorator
It is crucial to distinguish between the GoF Decorator Pattern and Python's @decorator syntax.
- GoF Decorator: A structural pattern dealing with objects. Wraps objects inside other objects to change behavior dynamically at runtime.
- Python
@decorator: A language feature dealing with functions/methods/classes. Wraps a function inside another function, usually applied at definition time.
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low - standard object composition. |
| SOLID | SRP (each decorator handles one specific behavior), OCP (add new decorators without altering existing code). |
| When Justified | When you need to combine behaviors dynamically and avoid subclass explosion. |
| When Over-engineered | If you only have one or two static variations, subclassing might be simpler. |
🌍 Real-World Framework Sightings
Python Standard Library
gzip.GzipFilecan wrap standard file objects to add compression transparently.- Standard decorators like
@classmethod,@staticmethod,@property.
Web Frameworks (Django, Flask, FastAPI)
- Middleware is a perfect example of the Decorator pattern. A request comes in, gets wrapped/modified by auth middleware, logging middleware, etc., before hitting the core view function.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Coffee Shop (Object Decorator) | Pure Python | The classic GoF Object Decorator for adding features dynamically. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Coffee Shop (Object Decorator)
Pattern: Decorator (Structural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You run a coffee shop. You have a base `Coffee` object.
Customers can add various condiments: Milk, Whip, Vanilla, Caramel.
Instead of creating a subclass for every combination (`MilkCoffee`,
`MilkVanillaCoffee`, `DoubleWhipCaramelCoffee`), we use decorators.
Each decorator wraps the previous object, adding to its cost
and description.
KEY INSIGHT:
------------
Decorators have the SAME INTERFACE as the objects they decorate.
This allows them to be stacked infinitely.
============================================================
"""
from typing import Protocol
# ─── Component Interface ───────────────────────────────
class Beverage(Protocol):
"""The Component interface that both base beverages and decorators implement."""
def get_description(self) -> str: ...
def cost(self) -> float: ...
# ─── Concrete Components (The Base Objects) ────────────
class DarkRoast:
def get_description(self) -> str:
return "Dark Roast Coffee"
def cost(self) -> float:
return 2.50
class Espresso:
def get_description(self) -> str:
return "Espresso"
def cost(self) -> float:
return 1.99
# ─── Decorator Base Class (Optional but helpful) ───────
class CondimentDecorator:
"""
Base Decorator.
It holds the wrapped component and implements the Beverage protocol.
(In Python, this base class is optional due to duck typing, but it
makes the pattern explicit).
"""
def __init__(self, beverage: Beverage) -> None:
self._beverage = beverage
def get_description(self) -> str:
return self._beverage.get_description()
def cost(self) -> float:
return self._beverage.cost()
# ─── Concrete Decorators ───────────────────────────────
class Milk(CondimentDecorator):
def get_description(self) -> str:
return f"{self._beverage.get_description()}, Milk"
def cost(self) -> float:
return self._beverage.cost() + 0.50
class Whip(CondimentDecorator):
def get_description(self) -> str:
return f"{self._beverage.get_description()}, Whip"
def cost(self) -> float:
return self._beverage.cost() + 0.70
class Vanilla(CondimentDecorator):
def get_description(self) -> str:
return f"{self._beverage.get_description()}, Vanilla Syrup"
def cost(self) -> float:
return self._beverage.cost() + 0.30
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("DECORATOR PATTERN - Coffee Shop")
print("=" * 60)
# 1. Plain Espresso
beverage1 = Espresso()
print(" [Order 1]")
print(f" Description: {beverage1.get_description()}")
print(f" Cost: ${beverage1.cost():.2f}")
# 2. Dark Roast with Double Milk and Whip
print("\n [Order 2 - Stacking Decorators]")
beverage2: Beverage = DarkRoast()
beverage2 = Milk(beverage2) # Wrap in Milk
beverage2 = Milk(beverage2) # Wrap in Milk again (Double Milk)
beverage2 = Whip(beverage2) # Wrap in Whip
print(f" Description: {beverage2.get_description()}")
print(f" Cost: ${beverage2.cost():.2f}")
# 3. Espresso with Vanilla and Milk
print("\n [Order 3]")
beverage3 = Milk(Vanilla(Espresso())) # You can nest them inline!
print(f" Description: {beverage3.get_description()}")
print(f" Cost: ${beverage3.cost():.2f}")
print("\n✅ Decorator pattern demonstrated successfully!")
Pattern 10: Facade (Structural)
🧒 The 5-Year-Old Explanation
When you want to watch a movie, you have to:
- Turn on the TV.
- Switch the TV input to HDMI 1.
- Turn on the soundbar.
- Set the soundbar volume to 50.
- Turn on the Apple TV.
- Open the Netflix app.
📦 Prerequisites
You should be comfortable with:
- Object Composition
- API Design (making things easy for the caller)
🔍 How to Spot This Pattern
Think of Facade when you see:
- You need to interact with a complex subsystem (many classes, many methods) but only need a simple, common use case.
- You want to structure a subsystem into layers.
- You want to decouple your client code from a messy 3rd party library by wrapping it in a clean, simple class.
- You are trying to add new functionality to an object → Decorator (Ch 09)
- You want to restrict access or add caching → Proxy (Ch 12)
- You need to adapt an incompatible interface to match an existing interface the client expects → Adapter (Ch 06)
🧩 The Core Idea
Intent (GoF)
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Facade |
Class | Knows which subsystem classes are responsible for a request. Delegates client requests to appropriate subsystem objects. |
Subsystem Classes |
Classes | Implement subsystem functionality. Handle work assigned by the Facade object. Have NO knowledge of the facade. |
📊 Visual Walkthrough
Adapter vs Facade vs Proxy
They all act as wrappers, but their intent is different:
🔄 Without Pattern → With Pattern
Level 1: Client Does All the Work
# BAD: Client code is coupled to the complex internals of the video encoding system
file = VideoFile("funny_cats.mp4")
source_codec = CodecFactory.extract(file)
destination_codec = MPEG4CompressionCodec()
buffer = BitrateReader.read(file, source_codec)
result = BitrateReader.convert(buffer, destination_codec)
audio = AudioMixer.fix(result)
Level 3: Facade Pattern
# GOOD: Client just calls one method. The Facade handles the complexity.
converter = VideoConverterFacade()
result = converter.convert("funny_cats.mp4", "mp4")
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Very Low. It's just a wrapper class. |
| SOLID | SRP (client code is simplified, facade handles orchestration), Law of Demeter (client talks to one friend instead of strangers). |
| When Justified | Hiding complex, ugly, or highly coupled subsystems behind a clean API. |
| When Over-engineered | If the subsystem is already simple. |
| Risk | The Facade can become a "God Object" coupled to every class in the app. |
🌍 Real-World Framework Sightings
requests (Python library)
- The
requests.get()function is a massive Facade. It hides the extreme complexity ofurllib3, connection pooling, SSL verification, cookie handling, and header construction.
Django / FastAPI
- A REST API endpoint is often a Facade. It takes a simple JSON request and internally orchestrates the Database (ORM), Authentication service, Cache, and Message Queue (Celery).
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Video Conversion | Pure Python | Hiding a complex subsystem of codecs and bitrates behind one method. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Video Conversion Facade
Pattern: Facade (Structural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You are building a video streaming app. You use a complex 3rd party
video conversion library (simulated here) with many classes:
VideoFile, CodecFactory, BitrateReader, AudioMixer, etc.
To convert a video, you have to orchestrate 6 different classes in
a specific order. If you put this logic in your UI code, your UI
becomes tightly coupled to the video library.
Instead, you build a `VideoConverterFacade` that exposes a single
method: `convert(filename, format)`.
KEY INSIGHT:
------------
The client only talks to the Facade. The Facade orchestrates the
subsystem. If the 3rd party library changes its API, you only have
to update the Facade, not your entire application.
============================================================
"""
# ─── COMPLEX SUBSYSTEM (Simulated 3rd Party Library) ───
class VideoFile:
def __init__(self, filename: str) -> None:
self.filename = filename
print(f" [Subsystem] Loaded video file: {filename}")
class OggCompressionCodec:
def get_type(self) -> str:
return "ogg"
class MPEG4CompressionCodec:
def get_type(self) -> str:
return "mp4"
class CodecFactory:
@staticmethod
def extract(file: VideoFile) -> str:
print(f" [Subsystem] Extracting codec from {file.filename}...")
return "mp4" if file.filename.endswith(".mp4") else "ogg"
class BitrateReader:
@staticmethod
def read(filename: str, source_codec: str) -> str:
print(f" [Subsystem] Reading file {filename} with codec {source_codec}...")
return f"buffer_of_{filename}"
@staticmethod
def convert(buffer: str, destination_codec: object) -> str:
print(f" [Subsystem] Converting {buffer} to target codec...")
return f"converted_{buffer}"
class AudioMixer:
@staticmethod
def fix_audio(result: str) -> str:
print(" [Subsystem] Fixing audio sync issues...")
return f"audio_synced_{result}"
# ─── FACADE ────────────────────────────────────────────
class VideoConverterFacade:
"""
The Facade. It hides the complexity of the subsystem from the client.
It provides a simple interface for the most common use case.
"""
def convert(self, filename: str, format: str) -> str:
print(f"\n🎬 Facade starting conversion of '{filename}' to '{format}'...")
# The Facade handles all the ugly orchestration!
file = VideoFile(filename)
source_codec = CodecFactory.extract(file)
if format == "mp4":
destination_codec = MPEG4CompressionCodec()
else:
destination_codec = OggCompressionCodec()
buffer = BitrateReader.read(filename, source_codec)
result = BitrateReader.convert(buffer, destination_codec)
final_result = AudioMixer.fix_audio(result)
print("🎬 Facade finished conversion!")
return final_result
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("FACADE PATTERN - Video Conversion")
print("=" * 60)
# Client Code
# Look how simple the client code is!
# It knows nothing about CodecFactory, AudioMixer, or BitrateReader.
converter = VideoConverterFacade()
# Convert an OGG to MP4
mp4_file = converter.convert("vacation_video.ogg", "mp4")
print(f"\nClient received: {mp4_file}")
print("\n✅ Facade pattern demonstrated successfully!")
Pattern 11: Flyweight (Structural)
🧒 The 5-Year-Old Explanation
Imagine you're drawing a forest with 1,000,000 trees. If you draw every single leaf and branch for every tree, your hand will cramp and you'll run out of paper (RAM).
Instead, you draw one perfect tree shape on a piece of cardboard and cut it out to make a stencil. Then, for the 1,000,000 trees, you just use the stencil and only record where it goes on the paper (X and Y coordinates).
You saved a ton of paper by sharing the heavy, detailed part (the stencil) and only keeping track of the small, changing parts (the coordinates). That's the Flyweight pattern!
📦 Prerequisites
You should be comfortable with:
- Memory management (how objects consume RAM)
- Intrinsic vs Extrinsic state
- Singleton pattern (Flyweight factories are often Singletons)
🔍 How to Spot This Pattern
Think of Flyweight when you see:
- An application needs to spawn a massive number of objects (e.g., bullets in a game, characters in a text editor, trees in a 3D landscape).
- The application is running out of RAM (OutOfMemoryException).
- The objects share a lot of duplicate state that can be factored out.
- You only have a few hundred objects. Modern computers have gigabytes of RAM. Don't optimize prematurely!
- The objects have completely unique states with no duplication.
🧩 The Core Idea
Intent (GoF)
Use sharing to support large numbers of fine-grained objects efficiently.
Intrinsic vs Extrinsic State
This is the most important concept in Flyweight. You must split your object's state into two pieces:
- Intrinsic State (The Stencil):
- Constant data that is the same across many objects.
- Stored inside the Flyweight object.
- Example: A tree's 3D mesh, texture, and color.
- Extrinsic State (The Coordinates):
- Data that is unique to each specific instance.
- Passed into the Flyweight methods from the outside.
- Example: A tree's X/Y coordinates on the map, its current health.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Flyweight |
Protocol / ABC |
Declares an interface through which flyweights can receive and act on extrinsic state. |
ConcreteFlyweight |
Class | Implements the interface and stores intrinsic state. Must be immutable! |
FlyweightFactory |
Class | Creates and manages flyweight objects. Ensures that flyweights are shared properly via a cache. |
Context |
Class | Contains the extrinsic state and a reference to the flyweight. |
📊 Visual Walkthrough
RAM Savings Visualization
🔄 Without Pattern → With Pattern
Level 1: Out of Memory
# BAD: We instantiate the heavy texture 1 million times.
class Particle:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.color = color
self.sprite = load_10mb_image() # 10MB per particle!
Level 3: Flyweight
# GOOD: The heavy data is shared.
class ParticleFlyweight:
def __init__(self, color):
self.color = color
self.sprite = load_10mb_image() # Loaded ONCE per color
class ParticleContext:
def __init__(self, x, y, flyweight: ParticleFlyweight):
self.x = x
self.y = y
self.flyweight = flyweight # Just a reference (8 bytes)
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | High - requires analyzing state and refactoring code to pass extrinsic state via method parameters. |
| RAM vs CPU | Trades RAM for CPU. You save RAM by sharing objects, but spend a tiny bit of CPU retrieving them from the factory and passing extrinsic data. |
| When Justified | STRICTLY an optimization pattern. Only use it when you actually run out of RAM due to massive numbers of similar objects. |
🌍 Real-World Framework Sightings
Python Standard Library
- Python does this internally with small integers (e.g.,
-5to256) and short strings (string interning).a = 100; b = 100; a is bisTruebecause Python uses a Flyweight cache for small ints!
Game Engines (Unity, Unreal)
- Rendering millions of grass blades or trees. The engine loads the mesh into the GPU once (Flyweight) and uses instanced rendering (passing an array of transforms/extrinsic state) to draw them all rapidly.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Forest Simulator | Pure Python | Separating heavy tree models (Intrinsic) from coordinates (Extrinsic). |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Forest Simulator (Tree Models vs Placements)
Pattern: Flyweight (Structural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You are building a game that needs to render a forest with millions
of trees. If every tree contains its own 3D mesh and texture data,
you will run out of RAM instantly.
Solution:
1. Extract the shared data (name, color, texture) into a Flyweight (`TreeModel`).
2. Keep the unique data (x, y coordinates) in a Context class (`Tree`).
3. Use a Factory to ensure we only create one `TreeModel` per type.
KEY INSIGHT:
------------
Notice how `draw()` requires the extrinsic state (canvas, x, y) to
be passed in as arguments. The Flyweight itself has no idea WHERE it is.
============================================================
"""
from typing import Dict, List
# ─── 1. Flyweight (Intrinsic State) ─────────────────────────
class TreeModel:
"""
The Flyweight. It contains state that is shared across many trees.
This object should be IMMUTABLE.
(Imagine `texture` is a massive 10MB byte array).
"""
def __init__(self, name: str, color: str, texture: str) -> None:
self.name = name
self.color = color
self.texture = texture
def draw(self, canvas: str, x: int, y: int) -> None:
"""
The operation. It takes the EXTRINSIC state (x, y) as arguments!
"""
print(f" 🌳 Drawing [{self.name}] colored '{self.color}' at ({x}, {y})")
# ─── 2. Flyweight Factory ──────────────────────────────────
class TreeFactory:
"""
Manages the Flyweights. Ensures we only ever create ONE instance
of a specific TreeModel.
"""
_tree_models: Dict[str, TreeModel] = {}
@classmethod
def get_tree_model(cls, name: str, color: str, texture: str) -> TreeModel:
# Create a unique key for this combination of intrinsic state
key = f"{name}_{color}_{texture}"
if key not in cls._tree_models:
print(f" [Factory] Cache miss: Creating new TreeModel for '{key}'")
cls._tree_models[key] = TreeModel(name, color, texture)
else:
print(f" [Factory] Cache hit: Returning existing TreeModel for '{key}'")
return cls._tree_models[key]
# ─── 3. Context (Extrinsic State) ──────────────────────────
class Tree:
"""
The Context. It contains the extrinsic state (x, y) and a reference
to the Flyweight (model).
This object is very lightweight (just a few bytes).
"""
def __init__(self, x: int, y: int, model: TreeModel) -> None:
self.x = x
self.y = y
self.model = model
def draw(self, canvas: str) -> None:
# Pass the extrinsic state to the Flyweight
self.model.draw(canvas, self.x, self.y)
# ─── 4. Client Code ────────────────────────────────────────
class Forest:
"""The client that spawns the objects."""
def __init__(self) -> None:
self.trees: List[Tree] = []
def plant_tree(self, x: int, y: int, name: str, color: str, texture: str) -> None:
# Get the flyweight from the factory (reuses if it already exists)
model = TreeFactory.get_tree_model(name, color, texture)
# Create the lightweight context object
tree = Tree(x, y, model)
self.trees.append(tree)
def draw(self, canvas: str) -> None:
for tree in self.trees:
tree.draw(canvas)
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("FLYWEIGHT PATTERN - Forest Simulator")
print("=" * 60)
forest = Forest()
print("\n [Planting Oaks]")
# Plant 3 Oaks. Notice the factory output: 1 cache miss, 2 cache hits.
forest.plant_tree(10, 20, "Oak", "Green", "oak_texture.png")
forest.plant_tree(15, 25, "Oak", "Green", "oak_texture.png")
forest.plant_tree(50, 80, "Oak", "Green", "oak_texture.png")
print("\n [Planting Pines]")
# Plant 2 Pines.
forest.plant_tree(100, 200, "Pine", "Dark Green", "pine_texture.png")
forest.plant_tree(105, 205, "Pine", "Dark Green", "pine_texture.png")
print("\n [Drawing the Forest]")
forest.draw("MainCanvas")
print("\n [Memory Analysis]")
num_trees = len(forest.trees)
num_models = len(TreeFactory._tree_models)
print(f" Total Trees (Contexts) created: {num_trees}")
print(f" Total TreeModels (Flyweights) in memory: {num_models}")
# We have 5 trees, but only 2 heavy model objects!
assert num_trees == 5
assert num_models == 2
# Prove that the oaks share the exact same model instance
oak1 = forest.trees[0]
oak2 = forest.trees[1]
assert oak1.model is oak2.model, "Flyweights are not being shared!"
print("\n✅ Flyweight pattern demonstrated successfully!")
Pattern 12: Proxy (Structural)
🧒 The 5-Year-Old Explanation
You want to buy a toy from the store. You can't just walk in and take it, and you don't have a credit card. So, you ask your dad. Your dad acts as your Proxy.
He checks if you have enough allowance (Security/Access Control). He checks if you already bought that toy yesterday (Caching). If everything is okay, he actually goes to the store and buys it for you (Delegation). The store thinks it's dealing with a customer, but it's really your dad standing in for you.
📦 Prerequisites
You should be comfortable with:
- The concept of interfaces (Protocols/ABCs) ensuring two classes look identical.
- Lazy evaluation (delaying heavy work until absolutely necessary).
🔍 How to Spot This Pattern
Think of Proxy when you see:
- You want to control access to an object (Security Proxy).
- You want to delay creating a heavy object until it's actually used (Virtual/Lazy Proxy).
- You want to cache the results of expensive operations on an object (Caching Proxy).
- You want to represent an object that is in a different address space, like across the network (Remote Proxy / RPC).
- You want to change the interface → Adapter (Ch 06)
- You want to simplify the interface → Facade (Ch 10)
- You want to add behavior dynamically (like sprinkles) → Decorator (Ch 09)
🧩 The Core Idea
Intent (GoF)
Provide a surrogate or placeholder for another object to control access to it.
UML Class Diagram
UML Sequence Diagram (Caching/Lazy Proxy)
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Subject |
Protocol / ABC |
Interface shared by RealSubject and Proxy. Allows the client to treat them identically. |
RealSubject |
Class | The real, heavy, or sensitive object that does the actual work. |
Proxy |
Class | Maintains a reference to the RealSubject. Controls access to it (creates it, deletes it, caches it, checks permissions). |
📊 Visual Walkthrough
The "Wrapper" Siblings (Recap)
Note: Proxy and Decorator look structurally identical in UML! The difference is entirely in their intent.
🔄 Without Pattern → With Pattern
Level 1: Doing the Heavy Lifting Immediately
# BAD: We connect to the DB immediately, even if we never run a query!
class DatabaseService:
def __init__(self):
self.connect_to_db() # Takes 5 seconds!
def query(self, sql):
return self.connection.execute(sql)
# Application startup is delayed by 5 seconds...
db = DatabaseService()
Level 3: Virtual Proxy (Lazy Initialization)
# GOOD: We only connect when the first query is run.
class DatabaseProxy(DatabaseInterface):
def __init__(self):
self._real_db = None # Don't connect yet!
def query(self, sql):
if self._real_db is None:
self._real_db = DatabaseService() # Connect NOW
return self._real_db.query(sql)
# Instant startup!
db = DatabaseProxy()
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low - standard wrapper class. |
| SOLID | SRP (separates security/caching from core logic), OCP (add new proxies without changing the real subject). |
| When Justified | When you want to add access control, lazy initialization, logging, or caching to an object without modifying its code. |
🌍 Real-World Framework Sightings
Django
django.utils.functional.LazyObjectis a virtual proxy. It delays evaluating something (like the current user or a translated string) until the absolute last moment when it's accessed.
SQLAlchemy
- When you define a relationship (
user.posts), SQLAlchemy doesn't load the posts immediately. It returns an "instrumented list" (a Proxy). When you actually loop overuser.posts, the Proxy intercepts the call, hits the database, and returns the real data (Lazy Loading).
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Protection Proxy | Pure Python | Checking permissions before delegating a request to the real subject. |
| 2 | Virtual Proxy (Lazy Loading) | Pure Python | Delaying the creation of a heavy object until it's actually needed. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Virtual & Protection Proxy
Pattern: Proxy (Structural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have a highly sensitive, very resource-intensive `DatabaseDownloader`
object.
1. It takes 3 seconds just to instantiate (establish connection).
2. It contains sensitive financial data.
We want to solve two problems:
- Don't establish the connection (instantiate) unless the user ACTUALLY
requests data (Virtual Proxy / Lazy Loading).
- Don't allow the request if the user doesn't have the "admin" role
(Protection Proxy).
KEY INSIGHT:
------------
The Proxy implements the exact same interface (`DownloaderProtocol`)
as the real object. The client has no idea it's talking to a proxy.
============================================================
"""
from typing import Protocol
import time
# ─── Subject (The Interface) ───────────────────────────
class DownloaderProtocol(Protocol):
"""The common interface for both the Real Subject and the Proxy."""
def download_financial_report(self, user_role: str) -> str: ...
# ─── Real Subject (The Heavy, Sensitive Object) ────────
class RealDatabaseDownloader:
"""
The RealSubject.
It takes a long time to initialize and does the actual heavy lifting.
"""
def __init__(self) -> None:
print(" [RealSubject] ⏳ Initializing database connection (this is slow)...")
time.sleep(1.5) # Simulated heavy init
print(" [RealSubject] ✅ Connection established!")
def download_financial_report(self, user_role: str) -> str:
# Note: The real subject doesn't check permissions.
# It assumes whoever calls it is allowed. That's the Proxy's job!
print(" [RealSubject] 💾 Downloading 500MB financial report...")
return "FINANCIAL_REPORT_Q3_2026.pdf"
# ─── Proxy (The Gatekeeper) ────────────────────────────
class DatabaseProxy:
"""
The Proxy.
It acts as BOTH a Virtual Proxy (lazy loading) AND a Protection Proxy (security).
"""
def __init__(self) -> None:
# We DO NOT instantiate the real subject yet! (Virtual Proxy)
self._real_downloader: RealDatabaseDownloader | None = None
def check_access(self, role: str) -> bool:
"""Protection Proxy logic."""
print(f" [Proxy] Checking access for role '{role}'...")
return role == "admin"
def download_financial_report(self, user_role: str) -> str:
# 1. Protection Proxy: Check permissions first
if not self.check_access(user_role):
print(f" [Proxy] ❌ Access DENIED for role '{user_role}'.")
return "ACCESS_DENIED"
print(f" [Proxy] 🔓 Access GRANTED for role '{user_role}'.")
# 2. Virtual Proxy: Lazy-load the real subject
if self._real_downloader is None:
print(" [Proxy] First request! Creating the RealSubject now...")
self._real_downloader = RealDatabaseDownloader()
else:
print(" [Proxy] RealSubject already exists. Using cached instance.")
# 3. Delegation: Pass the request to the real subject
return self._real_downloader.download_financial_report(user_role)
# ─── Client ────────────────────────────────────────────
def client_code(downloader: DownloaderProtocol, role: str) -> None:
"""
The client operates against the interface.
It doesn't know if 'downloader' is the RealSubject or the Proxy.
"""
print(f"\n--- Client requesting report as '{role}' ---")
result = downloader.download_financial_report(role)
print(f"Client received: {result}")
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("PROXY PATTERN - Virtual & Protection Proxy")
print("=" * 60)
# Instantiate the proxy. Notice how FAST this is!
# The expensive DB connection is NOT made yet.
print("Instantiating proxy...")
proxy = DatabaseProxy()
print("Proxy instantiated (almost instantly).\n")
# 1. Try as a guest (Protection Proxy blocks it)
# The real subject is NEVER created! We saved 1.5 seconds.
client_code(proxy, "guest")
# 2. Try as an admin (Protection Proxy allows it)
# The Virtual Proxy sees no real subject exists, so it creates it now.
client_code(proxy, "admin")
# 3. Try as an admin again
# The Virtual Proxy reuses the existing real subject. Very fast!
client_code(proxy, "admin")
print("\n✅ Proxy pattern demonstrated successfully!")
Pattern 13: Chain of Responsibility (Behavioral)
🧒 The 5-Year-Old Explanation
Imagine you are calling a big company's customer service. You talk to a robot first. If the robot can't help you, it passes you to a human agent. If the human agent can't help, they pass you to their manager.
You (the client) just made one request. The company has a Chain of people. The request travels down the chain until someone says, "I can handle this!" (or until the chain ends).
📦 Prerequisites
You should be comfortable with:
- Linked Lists (the structural foundation of this pattern)
- Recursion or while loops
- Object Composition (holding a reference to the
nexthandler)
🔍 How to Spot This Pattern
Think of Chain of Responsibility when you see:
- You have a request, and multiple objects could potentially process it.
- You don't want the sender of the request to know exactly which object will process it.
- You want to execute multiple handlers in a specific order (like middleware).
- You see
if/elif/elif/elifchains where each block represents a totally different subsystem trying to handle the same data.
- The sender needs an immediate, guaranteed return value from a specific receiver.
- Every receiver must process every request simultaneously (use Observer/PubSub instead).
🧩 The Core Idea
Intent (GoF)
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Handler |
Protocol / ABC |
Defines an interface for handling requests. (Optional: implements the successor link logic). |
ConcreteHandler |
Class | Handles requests it is responsible for. Can access its successor. If it can handle the request, it does so; otherwise, it forwards the request to its successor. |
Client |
Class/Script | Initiates the request to a Handler object on the chain. |
📊 Visual Walkthrough
The Two Variations of the Chain
There are two primary ways the chain can operate:
- The "One-and-Done" Chain (GoF Classic): The request travels down the chain. The FIRST handler that can process it does so, and the chain STOPS. (e.g., Customer Support).
- The "Middleware" Chain (Modern): The request travels down the chain. EVERY handler does a little piece of processing, modifies the request, and passes it to the next. (e.g., Express.js / Django middleware).
🔄 Without Pattern → With Pattern
Level 1: Monolithic if/elif hell
# BAD: The client is tightly coupled to every single handler.
def handle_support_ticket(ticket):
if ticket.type == "password_reset":
return RobotHandler().reset_password(ticket)
elif ticket.type == "billing" and ticket.amount < 100:
return Tier1Agent().process_refund(ticket)
elif ticket.type == "billing" and ticket.amount >= 100:
return Manager().process_refund(ticket)
# The function grows forever...
Level 3: Chain of Responsibility
# GOOD: Client only knows about the front of the chain.
# The chain configures itself.
robot = RobotHandler()
agent = Tier1Agent()
manager = Manager()
robot.set_next(agent).set_next(manager) # Build the chain
# Client just throws the ticket at the robot. It figures it out.
robot.handle(ticket)
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low - standard linked list setup. |
| SOLID | SRP (each handler does one specific check/task), OCP (easy to add new handlers without changing existing code or client). |
| When Justified | You need to process a request sequentially through multiple potential handlers, and the order or exact handlers might change dynamically. |
| Risk | A request might reach the end of the chain unhandled, failing silently if not caught. |
🌍 Real-World Framework Sightings
Web Frameworks (Django, FastAPI, Flask, Express)
- Middleware: A classic implementation of the Chain of Responsibility. An incoming HTTP request passes through CORS checks, Authentication, CSRF protection, and Logging before finally reaching your view function.
Python standard library
logging: When you log a message, it is passed to aLogger. If that logger doesn't handle it (or is set topropagate=True), it passes the message up the chain to the parent logger, all the way to the root logger.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Customer Support (Classic) | Pure Python | The request stops at the first handler that can process it. |
| 2 | Web Middleware (Modern) | Pure Python | The request passes through all handlers sequentially. |
⚡ Interactive Python 3.14 Implementations (2)
"""
============================================================
Example: Customer Support Chain (Classic GoF)
Pattern: Chain of Responsibility (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
A customer submits a support ticket with a certain severity.
- Severity 1 (Low): Handled by a Chatbot.
- Severity 2 (Medium): Handled by a Human Agent.
- Severity 3 (High): Handled by a Manager.
The client just sends the ticket to the first handler. If that
handler can't process it, it passes it down the chain. The FIRST
handler that can resolve the ticket does so, and the chain stops.
KEY INSIGHT:
------------
Notice the `BaseHandler` class. It implements the boilerplate for
managing the linked list (`set_next`). The concrete handlers only
need to implement the `handle()` logic.
============================================================
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class SupportTicket:
id: int
severity: int
description: str
# ─── Handler Interface / Base Class ─────────────────────
class Handler(ABC):
"""
The interface and base class for the Chain.
It manages the `_next_handler` reference.
"""
def __init__(self) -> None:
self._next_handler: Optional["Handler"] = None
def set_next(self, handler: "Handler") -> "Handler":
"""
Sets the next handler in the chain.
Returning the handler allows for a fluent interface:
h1.set_next(h2).set_next(h3)
"""
self._next_handler = handler
return handler
@abstractmethod
def handle(self, ticket: SupportTicket) -> Optional[str]:
"""
If this handler can't process the ticket, it delegates
to the next handler (if one exists).
"""
if self._next_handler:
return self._next_handler.handle(ticket)
# End of the chain, no one handled it
return None
# ─── Concrete Handlers ──────────────────────────────────
class ChatbotHandler(Handler):
def handle(self, ticket: SupportTicket) -> Optional[str]:
if ticket.severity == 1:
print(f" 🤖 [Chatbot] Handling ticket {ticket.id}: {ticket.description}")
return f"Chatbot resolved ticket {ticket.id} automatically."
# Not severity 1, pass it down the chain!
print(f" 🤖 [Chatbot] Cannot handle severity {ticket.severity}. Passing to Agent...")
return super().handle(ticket)
class HumanAgentHandler(Handler):
def handle(self, ticket: SupportTicket) -> Optional[str]:
if ticket.severity == 2:
print(f" 🧑💼 [Human Agent] Handling ticket {ticket.id}: {ticket.description}")
return f"Agent resolved ticket {ticket.id}."
print(f" 🧑💼 [Human Agent] Cannot handle severity {ticket.severity}. Passing to Manager...")
return super().handle(ticket)
class ManagerHandler(Handler):
def handle(self, ticket: SupportTicket) -> Optional[str]:
if ticket.severity >= 3:
print(f" 👔 [Manager] Handling urgent ticket {ticket.id}: {ticket.description}")
return f"Manager resolved urgent ticket {ticket.id}."
return super().handle(ticket)
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("CHAIN OF RESPONSIBILITY - Classic (Support Tickets)")
print("=" * 60)
# 1. Instantiate the handlers
chatbot = ChatbotHandler()
agent = HumanAgentHandler()
manager = ManagerHandler()
# 2. Wire the chain together: Chatbot -> Agent -> Manager
chatbot.set_next(agent).set_next(manager)
# 3. Create some tickets
tickets = [
SupportTicket(101, 1, "How do I reset my password?"),
SupportTicket(102, 2, "I was overcharged on my last bill."),
SupportTicket(103, 3, "Production server is ON FIRE!"),
SupportTicket(104, 0, "Wait, severity 0?"), # An unhandled case
]
# 4. Client just sends all tickets to the FRONT of the chain (chatbot)
for ticket in tickets:
print(f"\n--- Submitting Ticket {ticket.id} (Severity {ticket.severity}) ---")
result = chatbot.handle(ticket)
if result:
print(f" ✅ Result: {result}")
else:
print(f" ❌ UNHANDLED: No one in the chain could handle ticket {ticket.id}!")
print("\n✅ Chain of Responsibility demonstrated successfully!")
"""
============================================================
Example: Web Middleware Chain (Modern)
Pattern: Chain of Responsibility (Behavioral)
Framework: Pure Python (FastAPI/Django style)
============================================================
REAL-WORLD SCENARIO:
--------------------
A modern web server receives an HTTP request. Before it reaches
the actual application logic (the view), it needs to pass through:
1. Logging Middleware
2. Authentication Middleware
3. Payload Validation Middleware
Unlike the classic GoF pattern (where the chain stops at the FIRST
handler that can process it), this "Middleware" chain passes the
request through EVERY handler sequentially. Each handler can:
- Modify the request/response
- Stop the chain early (e.g., if Auth fails)
- Do work *after* the next handler returns (onion routing)
KEY INSIGHT:
------------
This is the architecture used by almost every modern web framework
(Django, Express, FastAPI, Starlette). It is a variation of the
Chain of Responsibility pattern.
============================================================
"""
from typing import Any, Callable, Dict
# The "Request" and "Response" objects
Request = Dict[str, Any]
Response = Dict[str, Any]
# A handler is a function that takes a Request and returns a Response
HandlerFunc = Callable[[Request], Response]
# ─── Middleware Base Class ─────────────────────────────
class Middleware:
"""
The base class for our chain.
It holds a reference to the `next` middleware (or the final app).
"""
def __init__(self) -> None:
self._next_app: HandlerFunc | None = None
def set_next(self, next_app: HandlerFunc) -> None:
self._next_app = next_app
def __call__(self, request: Request) -> Response:
"""
By implementing __call__, our middleware acts like a function.
Subclasses MUST implement this and call `self._next_app(request)`
when they are ready to pass the request down the chain.
"""
raise NotImplementedError()
# ─── Concrete Middleware (The Handlers) ────────────────
class LoggingMiddleware(Middleware):
def __call__(self, request: Request) -> Response:
print(f" 📝 [Logging] Received request to {request.get('path')}")
# Pass to the next handler...
if self._next_app:
response = self._next_app(request)
else:
response = {"status": 500, "body": "No app attached"}
# We can also do work ON THE WAY BACK UP! (Onion routing)
print(f" 📝 [Logging] Sending response with status {response.get('status')}")
return response
class AuthMiddleware(Middleware):
def __call__(self, request: Request) -> Response:
print(" 🔒 [Auth] Checking headers...")
token = request.get("headers", {}).get("Authorization")
if token != "Bearer valid_token":
print(" 🔒 [Auth] ❌ Invalid token! Halting the chain.")
# SHORT-CIRCUIT: We DO NOT call the next app!
return {"status": 401, "body": "Unauthorized"}
print(" 🔒 [Auth] ✅ Token valid. Passing down chain.")
if self._next_app:
return self._next_app(request)
return {"status": 500, "body": "No app attached"}
class ValidationMiddleware(Middleware):
def __call__(self, request: Request) -> Response:
print(" ✅ [Validation] Checking payload...")
if "body" not in request:
print(" ✅ [Validation] ❌ Missing body! Halting the chain.")
return {"status": 400, "body": "Bad Request"}
print(" ✅ [Validation] Payload valid. Passing down chain.")
if self._next_app:
return self._next_app(request)
return {"status": 500, "body": "No app attached"}
# ─── The Final Application (End of the Chain) ──────────
def core_application(request: Request) -> Response:
"""
The actual view function. It assumes all middleware checks passed.
"""
print(f" ⚙️ [App] Processing valid, authenticated request: {request['body']}")
return {"status": 200, "body": "Success!"}
# ─── Chain Builder Utility ─────────────────────────────
def build_middleware_chain(middlewares: list[Middleware], app: HandlerFunc) -> HandlerFunc:
"""
Helper function to wire the chain together backwards.
M1 -> M2 -> M3 -> App
"""
current_app = app
for middleware in reversed(middlewares):
middleware.set_next(current_app)
current_app = middleware
return current_app
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("CHAIN OF RESPONSIBILITY - Web Middleware")
print("=" * 60)
# 1. Build the chain
chain = build_middleware_chain(
middlewares=[
LoggingMiddleware(),
AuthMiddleware(),
ValidationMiddleware()
],
app=core_application
)
# 2. Test Case 1: Bad Auth (Should halt at AuthMiddleware)
print("\n--- Test 1: Bad Token ---")
bad_req = {
"path": "/api/data",
"headers": {"Authorization": "Bearer BAD_TOKEN"},
"body": {"data": 123}
}
resp1 = chain(bad_req)
print(f"Result: {resp1}")
# 3. Test Case 2: Bad Payload (Should halt at ValidationMiddleware)
print("\n--- Test 2: Bad Payload ---")
bad_payload_req = {
"path": "/api/data",
"headers": {"Authorization": "Bearer valid_token"}
# missing "body"
}
resp2 = chain(bad_payload_req)
print(f"Result: {resp2}")
# 4. Test Case 3: Perfect Request (Should reach the App)
print("\n--- Test 3: Perfect Request ---")
good_req = {
"path": "/api/data",
"headers": {"Authorization": "Bearer valid_token"},
"body": {"data": 123}
}
resp3 = chain(good_req)
print(f"Result: {resp3}")
print("\n✅ Middleware Chain demonstrated successfully!")
Pattern 14: Command (Behavioral)
🧒 The 5-Year-Old Explanation
When you order food at a restaurant, you don't walk into the kitchen and tell the chef how to cook your burger. Instead, you give an Order (a piece of paper) to the waiter. The waiter takes that piece of paper, puts it on a queue in the kitchen, and later, the chef reads it and cooks the food.
The Command pattern is that piece of paper. It takes a request (make a burger) and turns it into a standalone object. Because it's an object, you can put it in a list (queues), save it for later (scheduling), or even throw it in the trash if you change your mind (undo).
📦 Prerequisites
You should be comfortable with:
- First-class functions (in Python, functions are objects, which can sometimes replace this pattern).
- Object composition.
- The concept of Undo/Redo stacks.
🔍 How to Spot This Pattern
Think of Command when you see:
- You want to parameterize objects with operations (e.g., UI buttons that trigger different actions).
- You want to queue operations, schedule their execution, or execute them remotely.
- You need to implement Undo/Redo functionality.
- You want to log changes so they can be reapplied later (Event Sourcing / Transaction logs).
- The operation is very simple and executed immediately. (Just call the method directly!)
- You are writing Python and just need to pass a simple callback. (Use a lambda or a function reference instead of a full Command class).
🧩 The Core Idea
Intent (GoF)
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Command |
Protocol / ABC |
Declares an interface for executing an operation (usually just execute() and undo()). |
ConcreteCommand |
Class | Defines a binding between a Receiver object and an action. Implements execute() by invoking the corresponding operations on the Receiver. |
Client |
Class/Script | Creates a ConcreteCommand object and sets its receiver. |
Invoker |
Class | Asks the command to carry out the request. (Does NOT know how the command works, just calls execute()). |
Receiver |
Class | Knows how to perform the actual operations. Any class can serve as a Receiver. |
🔄 Without Pattern → With Pattern
Level 1: Tightly Coupled UI
# BAD: The UI Button is tightly coupled to the Business Logic (Document).
class Button:
def __init__(self, document: Document):
self.doc = document
def click(self):
self.doc.save() # The Button knows exactly what it's doing
Level 3: Command Pattern
# GOOD: The Button just holds a Command. It doesn't know what the Command does.
class Button:
def __init__(self, command: Command):
self.command = command
def click(self):
self.command.execute() # Could be Save, Print, Delete, anything!
save_cmd = SaveCommand(document)
btn = Button(save_cmd)
🐍 Python-Specific Note: Functions as Commands
Because Python treats functions as first-class objects, you often don't need a full Command class for simple use cases. You can just pass a function:
class Button:
def __init__(self, on_click: Callable):
self.on_click = on_click
def click(self):
self.on_click()
btn = Button(lambda: document.save())
- When you need
undo()functionality (the class stores the previous state). - When the command needs to be serialized/saved to a database (Event Sourcing).
- When the command requires complex parameterization that spans multiple methods.
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Medium - introduces many small classes (one for every action). |
| SOLID | SRP (decouples the class that invokes the operation from the class that performs it), OCP (you can add new commands without breaking existing code). |
| When Justified | Undo/Redo systems, Macro recording, Task scheduling, transaction logging. |
🌍 Real-World Framework Sightings
Django / Celery
- Celery Tasks: When you call
my_task.delay(args), you are creating a Command object, serializing it, and putting it on a queue (Redis/RabbitMQ). A worker (the Invoker) later pops it off and executes it.
Text Editors
- Every action (type a letter, delete a line, paste) is encapsulated in a Command object. The editor keeps a stack of these objects. When you press
Ctrl+Z, it pops the last command off the stack and callscommand.undo().
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Smart Home Remote | Pure Python | Parameterizing an invoker (remote) with commands. |
| 2 | Text Editor Undo/Redo | Pure Python | Using commands to implement history stacks. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Text Editor Undo/Redo
Pattern: Command (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You are building a text editor. You need to support Ctrl+Z (Undo)
and Ctrl+Y (Redo).
To do this, every modification to the text cannot just be a simple
string mutation. Instead, every action (typing, deleting, pasting)
must be encapsulated in a `Command` object that knows how to:
1. `execute()`: Apply the change.
2. `undo()`: Reverse the change.
The editor keeps two stacks: a History stack (for undo) and a
Redo stack.
KEY INSIGHT:
------------
This is the absolute best, textbook use-case for the Command pattern.
A simple Python callback/lambda cannot easily store the state required
to reverse an action. A Command class handles this perfectly.
============================================================
"""
from abc import ABC, abstractmethod
# ─── Receiver ──────────────────────────────────────────
class TextDocument:
"""The Receiver. The object being manipulated."""
def __init__(self) -> None:
self.text = ""
def __str__(self) -> str:
return f"Document: '{self.text}'"
# ─── Command Interface ─────────────────────────────────
class Command(ABC):
"""The Command interface."""
def __init__(self, document: TextDocument) -> None:
self.document = document
@abstractmethod
def execute(self) -> None:
"""Applies the command."""
pass
@abstractmethod
def undo(self) -> None:
"""Reverses the command."""
pass
# ─── Concrete Commands ─────────────────────────────────
class TypeTextCommand(Command):
def __init__(self, document: TextDocument, text_to_add: str) -> None:
super().__init__(document)
self.text_to_add = text_to_add
def execute(self) -> None:
self.document.text += self.text_to_add
print(f" [Command] Typed: '{self.text_to_add}'")
def undo(self) -> None:
# To undo, we slice off the length of the text we added
slice_length = len(self.text_to_add)
self.document.text = self.document.text[:-slice_length]
print(f" [Command] Undid typing: removed '{self.text_to_add}'")
class DeleteTextCommand(Command):
def __init__(self, document: TextDocument, length_to_delete: int) -> None:
super().__init__(document)
self.length_to_delete = length_to_delete
self._deleted_text = "" # State required for undo!
def execute(self) -> None:
# Save what we are about to delete so we can undo it later
if len(self.document.text) >= self.length_to_delete:
self._deleted_text = self.document.text[-self.length_to_delete:]
self.document.text = self.document.text[:-self.length_to_delete]
else:
self._deleted_text = self.document.text
self.document.text = ""
print(f" [Command] Deleted: '{self._deleted_text}'")
def undo(self) -> None:
# To undo a deletion, we just append the saved text back
self.document.text += self._deleted_text
print(f" [Command] Undid deletion: restored '{self._deleted_text}'")
# ─── Invoker ───────────────────────────────────────────
class TextEditor:
"""The Invoker. Manages command execution and the history stacks."""
def __init__(self, document: TextDocument) -> None:
self.document = document
self.history: list[Command] = []
self.redo_stack: list[Command] = []
def execute_command(self, command: Command) -> None:
"""Executes a command and pushes it to history."""
command.execute()
self.history.append(command)
# Any new action clears the redo stack!
self.redo_stack.clear()
def undo(self) -> None:
"""Pops the last command, calls undo(), and pushes to redo stack."""
if not self.history:
print(" [Editor] Nothing to undo.")
return
command = self.history.pop()
command.undo()
self.redo_stack.append(command)
def redo(self) -> None:
"""Pops the last undone command, calls execute(), and pushes to history."""
if not self.redo_stack:
print(" [Editor] Nothing to redo.")
return
command = self.redo_stack.pop()
command.execute()
self.history.append(command)
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("COMMAND PATTERN - Undo/Redo History")
print("=" * 60)
doc = TextDocument()
editor = TextEditor(doc)
print("\n--- 1. Typing ---")
editor.execute_command(TypeTextCommand(doc, "Hello "))
editor.execute_command(TypeTextCommand(doc, "World!"))
print(f"Current State: {doc}")
print("\n--- 2. Deleting ---")
editor.execute_command(DeleteTextCommand(doc, 6)) # Delete "World!"
print(f"Current State: {doc}")
print("\n--- 3. Undo ---")
editor.undo() # Restores "World!"
print(f"Current State: {doc}")
editor.undo() # Removes "Hello "
print(f"Current State: {doc}")
print("\n--- 4. Redo ---")
editor.redo() # Types "Hello " again
print(f"Current State: {doc}")
print("\n--- 5. New Action Clears Redo Stack ---")
# We still have one thing we *could* redo (World!), but if we type
# something new, the redo stack is cleared (standard editor behavior).
editor.execute_command(TypeTextCommand(doc, "Python!"))
print(f"Current State: {doc}")
editor.redo() # Should do nothing
print("\n✅ Command pattern (Undo/Redo) demonstrated successfully!")
Pattern 15: Iterator (Behavioral)
🧒 The 5-Year-Old Explanation
Imagine you have a big box of Lego blocks, and you want to look at every single one. You don't need to know how the blocks are stacked inside the box; you just want a magic hand that hands you one block, then the next, until the box is empty.
The Iterator is that magic hand. It lets you loop through a collection of things without needing to know if they are stored in a simple list, a complex tree, or a messy pile.
📦 Prerequisites
You should be comfortable with:
- Python's
iter()andnext()dunder methods. - The
StopIterationexception. - Python generators (
yield).
🔍 How to Spot This Pattern
Think of Iterator when you see:
- You want to hide the complex underlying data structure (like a Tree or Graph) behind a simple
for item in collection:loop. - You want multiple ways to traverse the same collection (e.g., Depth-First vs Breadth-First search).
- You want multiple independent traversals happening at the same time (two different iterators on the same collection).
- The collection is just a standard Python list and you only ever traverse it sequentially. (Python's built-in iterator is all you need).
🧩 The Core Idea
Intent (GoF)
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
UML Class Diagram
UML Sequence Diagram
🐍 Python-Specific Implementation
In Python, the Iterator pattern is built directly into the language. You do not need to invent createiterator, hasnext, or get_next. You MUST use Python's native protocol:
| GoF Concept | Python Dunder Method | Notes |
|---|---|---|
IterableCollection |
_iter_(self) |
Returns an Iterator object. Triggered when you do for x in my_obj. |
Iterator |
_next_(self) |
Returns the next item. Raises StopIteration when done. |
Iterator |
_iter_(self) |
Iterators in Python must also be Iterable (they return self). |
🔄 Without Pattern → With Pattern
Level 1: Exposing the internal structure
# BAD: The client has to understand how the Binary Tree is built!
tree = BinaryTree()
node = tree.root
# Client has to manually write a recursive DFS function just to see the items
Level 3: Iterator Pattern (Pythonic)
# GOOD: The tree hides its complexity behind __iter__
tree = BinaryTree()
for item in tree: # Pure magic!
print(item)
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low (especially in Python using yield). |
| SOLID | SRP (extracts complex traversal algorithms out of the collection), OCP (you can add new traversal algorithms without modifying the collection). |
| When Justified | Hiding complex data structures (Trees, Graphs, custom APIs) behind standard for loops. |
🌍 Real-World Framework Sightings
Python Standard Library
- Literally everywhere. Lists, dicts, strings, sets, files,
range(). They all implement the Iterator protocol.
SQLAlchemy
- When you execute a query, it returns a
ResultProxy(an Iterator). As you loop over it, it fetches rows from the database in batches rather than loading 1,000,000 rows into RAM at once.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Tree Traversals (DFS vs BFS) | Pure Python | Implementing multiple iterators for a complex data structure using both Classes and Generators. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Tree Traversals (DFS vs BFS)
Pattern: Iterator (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have a complex data structure: A Tree representing a company's
management hierarchy.
You want to allow other developers to loop over this tree easily:
`for employee in company_tree:`
But a tree isn't a flat list. Do we go deep first (Depth-First Search)?
Or level by level (Breadth-First Search)?
The Iterator pattern allows us to provide multiple ways to traverse
the exact same data structure, completely hiding the complex algorithms
from the client.
KEY INSIGHT:
------------
We will implement one iterator the "Hard Way" (using a Class with
__iter__ and __next__) and one the "Pythonic Way" (using a generator
with `yield`). Both fulfill the Iterator pattern!
============================================================
"""
from collections import deque
from typing import Any, Generator, Iterator
# ─── Node ──────────────────────────────────────────────
class Node:
"""A node in our tree."""
def __init__(self, value: str) -> None:
self.value = value
self.children: list['Node'] = []
def add(self, child: 'Node') -> None:
self.children.append(child)
# ─── Iterator 1: The "Class" Way (Breadth-First) ───────
class BreadthFirstIterator(Iterator[Node]):
"""
A classic class-based iterator.
Implements BFS (Level Order Traversal) using a queue.
"""
def __init__(self, root: Node) -> None:
self._queue = deque([root]) if root else deque()
def __iter__(self) -> Iterator[Node]:
"""Iterators must return themselves."""
return self
def __next__(self) -> Node:
"""The core logic to get the next element."""
if not self._queue:
# This is how Python knows the loop is finished!
raise StopIteration
current = self._queue.popleft()
# Add children to the queue for later processing
for child in current.children:
self._queue.append(child)
return current
# ─── The Collection (Iterable) ─────────────────────────
class CompanyTree:
"""
The Iterable Collection.
It holds the data and provides methods to get Iterators.
"""
def __init__(self, root: Node) -> None:
self.root = root
def __iter__(self) -> Iterator[Node]:
"""
Default iteration method (used by `for x in tree:`).
We'll default to Breadth-First Search using our custom class.
"""
return BreadthFirstIterator(self.root)
def depth_first(self) -> Generator[Node, None, None]:
"""
Iterator 2: The "Pythonic" Way (Generators).
Using `yield` automatically creates an Iterator class behind the scenes!
"""
if not self.root:
return
# DFS uses a stack (LIFO)
stack = [self.root]
while stack:
current = stack.pop()
yield current # Pause execution and return the value!
# Add children to stack in reverse order so left is processed first
for child in reversed(current.children):
stack.append(child)
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("ITERATOR PATTERN - Tree Traversals")
print("=" * 60)
# 1. Build the Tree
# CEO
# / \
# VP1 VP2
# / \ \
# Dev1 Dev2 HR1
ceo = Node("CEO")
vp1 = Node("VP Engineering")
vp2 = Node("VP HR")
dev1 = Node("Dev 1")
dev2 = Node("Dev 2")
hr1 = Node("HR 1")
vp1.add(dev1)
vp1.add(dev2)
vp2.add(hr1)
ceo.add(vp1)
ceo.add(vp2)
company = CompanyTree(ceo)
# 2. Iterate using the Default Iterator (BFS / Level Order)
print("\n--- Default Iteration (Breadth-First via Class) ---")
print("Expected: CEO -> VP Eng -> VP HR -> Dev 1 -> Dev 2 -> HR 1")
for employee in company:
print(f" 🏢 {employee.value}")
# 3. Iterate using the Alternative Iterator (DFS / Pre-order)
print("\n--- Alternative Iteration (Depth-First via Generator) ---")
print("Expected: CEO -> VP Eng -> Dev 1 -> Dev 2 -> VP HR -> HR 1")
for employee in company.depth_first():
print(f" 🏢 {employee.value}")
print("\n✅ Iterator pattern demonstrated successfully!")
Pattern 16: Mediator (Behavioral)
🧒 The 5-Year-Old Explanation
Imagine an airport with 50 airplanes. If every airplane had to talk to every other airplane to avoid crashing, it would be chaos! Everyone would be screaming on the radio.
Instead, they all talk to the Air Traffic Controller (the Mediator). The airplanes only need to know how to talk to the tower. The tower figures out who can land and who needs to wait, and gives the orders.
📦 Prerequisites
You should be comfortable with:
- Object Composition
- The concept of tight coupling (Spaghetti code)
- Event-driven architecture (Signals, Callbacks)
🔍 How to Spot This Pattern
Think of Mediator when you see:
- A set of objects communicate in well-defined but complex ways. The interdependencies are unstructured and difficult to understand ("Spaghetti").
- Reusing an object is difficult because it refers to and communicates with many other objects.
- You have a GUI dialog box where clicking a checkbox disables a text field, which enables a submit button, which triggers a dropdown.
- The objects only communicate with one or two other objects in a very simple, linear way.
- You want objects to be notified of state changes without caring who the listeners are (Use Observer/PubSub instead).
🧩 The Core Idea
Intent (GoF)
Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Mediator |
Protocol / ABC |
Defines an interface for communicating with Component objects. |
ConcreteMediator |
Class | Implements cooperative behavior by coordinating Component objects. It knows and maintains its components. |
BaseComponent |
Class | Provides basic functionality for storing a mediator's reference inside component objects. |
Components |
Classes | Various classes that contain some business logic. They do not communicate with other components, only with the mediator. |
📊 Visual Walkthrough
The "Spaghetti" vs The "Star" Topology
🔄 Without Pattern → With Pattern
Level 1: Tight Coupling (Spaghetti)
# BAD: Checkbox knows about TextField. TextField knows about Button.
class Checkbox:
def check(self):
self.textfield.enable()
self.button.disable()
Level 3: Mediator Pattern
# GOOD: Checkbox only knows about the Dialog (Mediator)
class Checkbox:
def check(self):
self.mediator.notify(self, "checkbox_checked")
class DialogMediator:
def notify(self, sender, event):
if event == "checkbox_checked":
self.textfield.enable()
self.button.disable()
⚖️ Mediator vs Observer (PubSub)
These two are very similar and often confused.
| Feature | Mediator | Observer (PubSub) |
|---|---|---|
| Topology | Centralized (Star). The Mediator knows exactly who is involved. | Decentralized. Publishers don't know who the Subscribers are. |
| Coupling | Components are decoupled from each other, but coupled to the Mediator. | Publishers are entirely decoupled from Subscribers. |
| Use Case | Complex, multi-directional interactions (e.g., UI dialogs). | Simple one-way notifications (e.g., Event Streams). |
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Medium. |
| SOLID | SRP (extracts communication logic into one place), OCP (new mediators can be introduced without changing components). |
| Risk | The Mediator can easily become a God Object that knows too much and does too much. |
🌍 Real-World Framework Sightings
GUI Frameworks (Tkinter, PyQt)
- Complex dialog boxes often act as Mediators. When a user changes a setting, the dialog receives the event and updates other UI elements accordingly.
Django Models
- If you have highly coupled model
save()methods (e.g., User saves -> updates Profile -> updates Account), a Service Layer or Signal Dispatcher often acts as a Mediator to orchestrate these saves without the models directly importing each other.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Air Traffic Control | Pure Python | Centralizing communication between decoupled airplanes. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Air Traffic Control
Pattern: Mediator (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
Airplanes (Components) need to coordinate landing on a single
Runway. If Airplane A talks directly to Airplane B, Airplane C,
and the Runway, the code becomes tightly coupled spaghetti.
Instead, every Airplane communicates ONLY with the Control Tower
(Mediator). The Control Tower contains the logic to decide who
can land and when.
KEY INSIGHT:
------------
The `Airplane` class does not know about the `Runway` or other
`Airplanes`. It only knows about the `ControlTowerProtocol`.
============================================================
"""
from typing import Protocol
# ─── Mediator Interface ────────────────────────────────
class ControlTowerProtocol(Protocol):
"""The interface that Components use to talk to the Mediator."""
def notify(self, sender: 'Airplane', event: str) -> None: ...
# ─── Components ────────────────────────────────────────
class Airplane:
"""
A Component. It only knows about the Mediator.
It does NOT know about other airplanes or the runway.
"""
def __init__(self, flight_number: str) -> None:
self.flight_number = flight_number
self.mediator: ControlTowerProtocol | None = None
def set_mediator(self, mediator: ControlTowerProtocol) -> None:
self.mediator = mediator
def request_landing(self) -> None:
print(f" ✈️ [Flight {self.flight_number}] Requesting landing clearance...")
if self.mediator:
self.mediator.notify(self, "request_landing")
def finish_landing(self) -> None:
print(f" ✈️ [Flight {self.flight_number}] Has landed and cleared the runway.")
if self.mediator:
self.mediator.notify(self, "landed")
class Runway:
"""Another Component."""
def __init__(self) -> None:
self.is_clear = True
# ─── Concrete Mediator ─────────────────────────────────
class JFKControlTower:
"""
The Concrete Mediator.
It knows about all the components (Runway, Airplanes) and
contains the complex business logic of how they interact.
"""
def __init__(self) -> None:
self.runway = Runway()
self.waiting_queue: list[Airplane] = []
def notify(self, sender: Airplane, event: str) -> None:
"""The central hub for all communication."""
if event == "request_landing":
if self.runway.is_clear:
print(f" 🗼 [Tower] Runway is clear. Flight {sender.flight_number}, you are cleared to land.")
self.runway.is_clear = False
else:
print(f" 🗼 [Tower] Runway is busy! Flight {sender.flight_number}, please hold.")
self.waiting_queue.append(sender)
elif event == "landed":
self.runway.is_clear = True
# If someone is waiting, clear them to land now
if self.waiting_queue:
next_flight = self.waiting_queue.pop(0)
print(f" 🗼 [Tower] Runway now clear. Flight {next_flight.flight_number}, you are cleared to land.")
self.runway.is_clear = False
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("MEDIATOR PATTERN - Air Traffic Control")
print("=" * 60)
# 1. Create the Mediator
tower = JFKControlTower()
# 2. Create the Components
flight1 = Airplane("AA101")
flight2 = Airplane("DL404")
flight3 = Airplane("UA909")
# 3. Connect components to the Mediator
flight1.set_mediator(tower)
flight2.set_mediator(tower)
flight3.set_mediator(tower)
# 4. Action!
# Flight 1 requests to land. Runway is clear.
flight1.request_landing()
# Flight 2 requests to land. Runway is busy, so it holds.
flight2.request_landing()
# Flight 3 requests to land. Runway is busy, so it holds.
flight3.request_landing()
# Flight 1 finishes landing. The tower automatically notifies Flight 2.
flight1.finish_landing()
# Flight 2 finishes landing. The tower automatically notifies Flight 3.
flight2.finish_landing()
flight3.finish_landing()
print("\n✅ Mediator pattern demonstrated successfully!")
Pattern 17: Memento (Behavioral)
🧒 The 5-Year-Old Explanation
Imagine you are playing a very hard video game. Before you walk into the boss room, you hit Save Game. You fight the boss, and you die. You hit Load Game, and you are right back before the boss room with all your health and items.
The game didn't just remember that you "walked forward" (like the Command pattern). It took a total snapshot of everything in your character's pockets and saved it in a memory box (a Memento).
📦 Prerequisites
You should be comfortable with:
- Encapsulation (Private variables).
- The difference between shallow and deep copies.
🔍 How to Spot This Pattern
Think of Memento when you see:
- You need to produce snapshots of an object's state to be able to restore a previous state (Save/Load functionality).
- You want to implement Undo/Redo, but the
Commandpattern is too complicated because the state changes are massive and unpredictable. - Direct access to the object's fields/getters/setters would violate encapsulation.
- The object's state is tiny and public, so you can just read it normally.
- You can easily calculate the reverse of an action (e.g.,
x + 5reverses tox - 5), in which caseCommandis much more efficient than saving the entire state.
🧩 The Core Idea
Intent (GoF)
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Originator |
Class | The object whose state we want to save (e.g., The Game). It creates a Memento containing a snapshot of its current internal state. |
Memento |
Class | A value object (often a Dataclass) that stores the Originator's state. It protects against access by objects other than the Originator (strict encapsulation). |
Caretaker |
Class | The object that knows when and why to capture the Originator's state, and keeps track of the Mementos. It never examines the contents of a Memento. |
⚖️ Memento vs Command for Undo/Redo
Both patterns can be used for Undo/Redo. Which one should you pick?
| Feature | Command Pattern (Delta) | Memento Pattern (Snapshot) |
|---|---|---|
| How it works | Stores the action taken (+5) |
Stores the entire state (score=10) |
| RAM Usage | Very Low (just stores the delta) | High (copies the whole object every time) |
| Encapsulation | May require exposing state to Commands | Strict. Only the Originator touches its state. |
| Use Case | Text editors, vector drawing apps. | Video games saves, database checkpoints. |
🔄 Without Pattern → With Pattern
Level 1: Violating Encapsulation
# BAD: The Caretaker has to manually reach in and grab private fields.
# If the Game adds a new field (e.g., `mana`), the Caretaker breaks!
class Game:
def __init__(self):
self._health = 100
self._level = 1
class Caretaker:
def save(self):
# The Caretaker shouldn't know these details!
self.save_file = (game._health, game._level)
Level 3: Memento Pattern
# GOOD: The Game handles its own snapshotting. The Caretaker is blind.
class Game:
def save(self) -> GameMemento:
return GameMemento(self._health, self._level) # Memento is opaque
class Caretaker:
def save(self):
self.save_file = game.save() # Caretaker just holds the box
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low - just saving state to a dataclass. |
| RAM Usage | Very High. Creating deep copies of massive objects repeatedly consumes lots of memory. |
| SOLID | SRP (separates state-saving logic from the business logic). |
🌍 Real-World Framework Sightings
Standard Python
- Python's
picklemodule is essentially a massive, dynamic Memento system. It serializes an object's exact state to bytes, which can be stored (Caretaker) and loaded later.
Databases
- Database transactions use a form of Memento. When you start a transaction, the DB takes a snapshot of the relevant state. If you
ROLLBACK, it restores the state from that snapshot.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Video Game Saves | Pure Python | Taking snapshots (Mementos) and restoring them without breaking encapsulation. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Video Game Saves
Pattern: Memento (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You are playing an RPG game. Your character has health, mana,
coordinates, and a list of items.
You encounter a boss and hit "Quick Save" (F5).
You die. You hit "Quick Load" (F9) to restore your state.
KEY INSIGHT:
------------
The `GameCaretaker` manages the save files, but it HAS NO IDEA
what is inside them. Only the `RPGGame` (Originator) knows how
to create and read a `GameMemento`. This perfectly preserves
encapsulation!
============================================================
"""
from dataclasses import dataclass
import copy
# ─── Memento ───────────────────────────────────────────
@dataclass(frozen=True)
class GameMemento:
"""
The Memento.
It is completely immutable (frozen). It holds a snapshot of state.
Notice we don't expose any methods to alter this data.
"""
name: str # e.g., "Save 1 - Boss Room"
health: int
mana: int
level: int
inventory: list[str]
# ─── Originator ────────────────────────────────────────
class RPGGame:
"""
The Originator.
This is the actual game. It has complex internal state.
It is the ONLY class that creates and consumes GameMementos.
"""
def __init__(self) -> None:
# Complex internal state
self._health = 100
self._mana = 50
self._level = 1
self._inventory = ["Sword", "Potion"]
def take_damage(self, amount: int) -> None:
self._health -= amount
print(f" ⚔️ Took {amount} damage! Health is now {self._health}.")
def level_up(self) -> None:
self._level += 1
self._health = 100
self._mana = 50
print(f" ✨ Leveled up to {self._level}!")
def loot(self, item: str) -> None:
self._inventory.append(item)
print(f" 💰 Looted: {item}")
def __str__(self) -> str:
return (f"Game State -> Level: {self._level} | "
f"HP: {self._health} | MP: {self._mana} | "
f"Inv: {self._inventory}")
# --- Memento Logic ---
def save_game(self, save_name: str) -> GameMemento:
"""Creates a snapshot of the current state."""
print(f" 💾 Saving game: '{save_name}'...")
# Note: We must DEEP COPY mutable objects like lists!
return GameMemento(
name=save_name,
health=self._health,
mana=self._mana,
level=self._level,
inventory=copy.deepcopy(self._inventory)
)
def load_game(self, memento: GameMemento) -> None:
"""Restores the state from a snapshot."""
print(f" 🔄 Loading game: '{memento.name}'...")
self._health = memento.health
self._mana = memento.mana
self._level = memento.level
self._inventory = copy.deepcopy(memento.inventory)
# ─── Caretaker ─────────────────────────────────────────
class GameCaretaker:
"""
The Caretaker.
It manages the save files (mementos), but NEVER looks inside them.
It just holds onto them and gives them back to the Game when asked.
"""
def __init__(self, game: RPGGame) -> None:
self._game = game
self._saves: list[GameMemento] = []
def quick_save(self) -> None:
save_name = f"QuickSave_Slot_{len(self._saves) + 1}"
memento = self._game.save_game(save_name)
self._saves.append(memento)
def quick_load(self) -> None:
if not self._saves:
print(" ❌ No save files found!")
return
memento = self._saves.pop() # Load the most recent save
self._game.load_game(memento)
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("MEMENTO PATTERN - Video Game Saves")
print("=" * 60)
game = RPGGame()
caretaker = GameCaretaker(game)
print("\n[Starting Game]")
print(f" {game}")
print("\n[Progressing...]")
game.level_up()
game.loot("Dragon Shield")
print(f" {game}")
print("\n[Before Boss Room - Quick Save!]")
caretaker.quick_save()
print("\n[Fighting Boss...]")
game.take_damage(80)
game.loot("Cursed Ring")
print(f" {game}")
game.take_damage(50) # Dead!
print(f" {game} (DEAD!)")
print("\n[Rage Quit & Quick Load!]")
caretaker.quick_load()
print(f" {game}")
print("\n✅ Memento pattern demonstrated successfully!")
Pattern 18: Observer (Behavioral)
🧒 The 5-Year-Old Explanation
Imagine you are waiting for a new video game to come out at a store.
Without Observer: You go to the store every single day and ask, "Is it here yet?" The cashier says "No." You waste a lot of time checking. (This is called Polling).
With Observer: You give the cashier your phone number. You stay home and play outside. When the game arrives, the cashier sends a text to everyone on the list. You only go to the store when you get the text. (This is the Observer pattern / PubSub).
📦 Prerequisites
You should be comfortable with:
- Lists/Sets to hold references to objects.
- First-class functions (Callbacks) in Python.
- Event-driven architecture.
🔍 How to Spot This Pattern
Think of Observer when you see:
- Changes to one object require changing others, and you don't know how many objects need to be changed.
- Some objects in your app must observe others, but only for a limited time or in specific cases.
- You have a core business logic class that is getting cluttered with UI update code.
- You see terms like:
Event,Listener,Subscriber,Publisher,Signal,Callback.
- The objects need to communicate back and forth in a complex, multi-directional way → Mediator (Ch 16).
🧩 The Core Idea
Intent (GoF)
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Subject (Publisher) |
Class | Issues events of interest to other objects. Keeps a list of subscribers. |
Observer (Subscriber) |
Protocol / Callable |
The interface that the Subject uses to notify subscribers. |
ConcreteObserver |
Class | Performs some actions in response to notifications issued by the publisher. |
🔄 Without Pattern → With Pattern
Level 1: Polling (The Anti-Pattern)
# BAD: The CPU is constantly spinning, checking for a change.
def wait_for_data(sensor):
while True:
data = sensor.get_data()
if data > 100:
trigger_alarm()
time.sleep(1)
Level 3: Observer Pattern
# GOOD: The sensor pushes data to us ONLY when it happens.
class Alarm:
def update(self, data):
if data > 100: trigger_alarm()
sensor.attach(Alarm())
# Now we do nothing. We just wait for the sensor to call Alarm.update()
🐍 Python-Specific Implementation
In statically typed languages (Java/C++), Observers are almost always Classes implementing an interface (e.g., void update(Event e)).
In Python, functions are first-class objects. Therefore, an Observer is often just a callback function. We don't need a whole class just to receive an event!
# Pythonic Observer
def on_data_received(data):
print(f"Got {data}")
subject.subscribe(on_data_received) # Pass the function directly!
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low - standard list of callbacks. |
| SOLID | OCP (you can introduce new subscriber classes without having to change the publisher's code). |
| Risk: Memory Leaks | The Lapsed Listener Problem. If a Subject holds a reference to an Observer, the Observer will NEVER be garbage collected. Always provide a detach() method, or use weakref in Python! |
| Risk: Ordering | Subscribers are notified in random/unpredictable order. Do not rely on order! |
🌍 Real-World Framework Sightings
Django Signals
django.db.models.signals.post_save: You can subscribe to this signal to execute code whenever ANY model is saved to the database, completely decoupling the save logic from the subsequent action (like sending a welcome email).
Frontend Frameworks (React, Vue)
- The entire concept of Reactive UI is built on the Observer pattern. When the state changes, the UI components observing that state are automatically re-rendered.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | YouTube Channel Subscriptions | Pure Python | The classic GoF Object-Oriented Observer with Classes. |
| 2 | Pythonic Event Emitter | Pure Python | Using functions/callbacks instead of Classes for a modern Python approach. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Pythonic Event Emitter
Pattern: Observer (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have a `ShoppingCart` object. When a user completes checkout,
several things need to happen:
1. Send an email receipt.
2. Update inventory.
3. Log the transaction.
Instead of writing all that logic inside `ShoppingCart.checkout()`,
we make the Cart an "Event Emitter" (Subject). It just broadcasts
the "checkout_complete" event. Other systems subscribe to this event.
KEY INSIGHT:
------------
Instead of using full classes for Observers (like the GoF book),
we use Python's first-class functions (callbacks). This is the
standard way to write Observers in modern Python.
============================================================
"""
from typing import Any, Callable, Dict, List
# Type alias for our observer callbacks
ObserverCallback = Callable[[Dict[str, Any]], None]
# ─── Subject (Publisher / Event Emitter) ───────────────
class EventEmitter:
"""
A generic Publisher that can handle multiple types of events.
(Very similar to Node.js EventEmitter).
"""
def __init__(self) -> None:
# A dictionary mapping event names to lists of callbacks
self._events: Dict[str, List[ObserverCallback]] = {}
def on(self, event_name: str, callback: ObserverCallback) -> None:
"""Subscribe to an event (Attach)."""
if event_name not in self._events:
self._events[event_name] = []
self._events[event_name].append(callback)
print(f" [System] Attached callback '{callback.__name__}' to event '{event_name}'")
def off(self, event_name: str, callback: ObserverCallback) -> None:
"""Unsubscribe from an event (Detach)."""
if event_name in self._events and callback in self._events[event_name]:
self._events[event_name].remove(callback)
print(f" [System] Detached callback '{callback.__name__}' from event '{event_name}'")
def emit(self, event_name: str, event_data: Dict[str, Any]) -> None:
"""Broadcast an event to all subscribers (Notify)."""
if event_name in self._events:
print(f"\n 📢 [EventEmitter] Broadcasting '{event_name}'...")
for callback in self._events[event_name]:
callback(event_data)
# ─── Concrete Subject (The Shopping Cart) ──────────────
class ShoppingCart(EventEmitter):
"""
Our specific business logic class. It inherits the emit/on methods.
"""
def __init__(self, user_id: str) -> None:
super().__init__()
self.user_id = user_id
self.items: List[str] = []
def add_item(self, item: str) -> None:
self.items.append(item)
def checkout(self) -> None:
print(f"\n 🛒 [Cart] {self.user_id} is checking out with {self.items}...")
# We do NOT send emails or update inventory here!
# We just EMIT the event and let the observers handle the rest.
event_data = {
"user_id": self.user_id,
"items": self.items,
"total_items": len(self.items)
}
self.emit("checkout_complete", event_data)
self.items.clear()
# ─── Observers (Just plain functions!) ─────────────────
def send_email_receipt(data: Dict[str, Any]) -> None:
print(f" ✉️ [EmailService] Sending receipt to {data['user_id']} for {data['total_items']} items.")
def update_inventory(data: Dict[str, Any]) -> None:
print(f" 📦 [InventoryService] Deducting stock for: {data['items']}")
def log_transaction(data: Dict[str, Any]) -> None:
print(f" 📝 [Logger] Transaction logged: User {data['user_id']} bought {data['total_items']} items.")
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("OBSERVER PATTERN - Pythonic Event Emitter")
print("=" * 60)
# 1. Setup the Subject
cart = ShoppingCart("Alice")
# 2. Attach Observers (Wire up the app)
cart.on("checkout_complete", send_email_receipt)
cart.on("checkout_complete", update_inventory)
cart.on("checkout_complete", log_transaction)
# 3. Use the app!
cart.add_item("Laptop")
cart.add_item("Mouse")
# When checkout runs, it will trigger all 3 observers automatically.
cart.checkout()
# 4. Detach an observer (Alice opts out of emails)
print("\n[Detaching Email Service]")
cart.off("checkout_complete", send_email_receipt)
cart.add_item("Keyboard")
cart.checkout() # Only Inventory and Logger will run this time!
print("\n✅ Observer pattern demonstrated successfully!")
Pattern 19: State (Behavioral)
🧒 The 5-Year-Old Explanation
Think about a Smartphone.
- If the phone is unlocked, pressing the home button goes to the home screen.
- If the phone is locked, pressing the home button brings up the password screen.
- If the phone is turned off, pressing the home button does absolutely nothing.
📦 Prerequisites
You should be comfortable with:
- Finite State Machines (FSM).
- Object Composition.
🔍 How to Spot This Pattern
Think of State when you see:
- An object behaves differently depending on its current state.
- Massive
if/elif/elifstatements inside multiple methods, where every condition checks the exact same state variable (e.g.,if self.state == "LOCKED": ...). - You need to build a State Machine (e.g., an Order going from Pending -> Paid -> Shipped -> Delivered).
- The object has only two states (e.g., True/False) and the behavior change is very simple. A boolean flag is fine; don't over-engineer.
- You want to change the algorithm used to perform a specific task, but the object itself hasn't fundamentally changed its status → Strategy (Ch 20).
🧩 The Core Idea
Intent (GoF)
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Context |
Class | Maintains an instance of a ConcreteState subclass that defines the current state. |
State |
Protocol / ABC |
Defines an interface for encapsulating the behavior associated with a particular state of the Context. |
ConcreteState |
Class | Each subclass implements a behavior associated with a state of the Context. Can trigger state transitions. |
📊 Visual Walkthrough
State vs Strategy
These two patterns look identical in UML, but they solve different problems.
🔄 Without Pattern → With Pattern
Level 1: The "if/elif" Hell
# BAD: Every single method has to check every single state.
class AudioPlayer:
def click_play(self):
if self.state == "LOCKED":
pass
elif self.state == "PLAYING":
self.pause()
self.state = "PAUSED"
elif self.state == "READY":
self.play()
self.state = "PLAYING"
def click_next(self):
if self.state == "LOCKED":
pass
elif self.state == "PLAYING":
self.next_song()
# ... it never ends
Level 3: State Pattern
# GOOD: Context delegates to the current state object
class AudioPlayer:
def click_play(self):
self.state.click_play() # The state object knows what to do!
class LockedState(State):
def click_play(self): pass # Do nothing
class ReadyState(State):
def click_play(self):
self.player.play()
self.player.change_state(PlayingState()) # Transition!
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | High - creates many new classes for a single Context. |
| SOLID | SRP (organizes state-specific code into separate classes), OCP (new states can be added easily without changing existing state classes or the context). |
| When Justified | Finite State Machines with 3+ states, where behavior changes drastically per state, and transitions are complex. |
🌍 Real-World Framework Sightings
TCP/IP Networking
- The classic example. A TCP connection has states like
LISTEN,ESTABLISHED,FIN_WAIT. Callingclose()on aLISTENsocket does something entirely different than calling it on anESTABLISHEDsocket.
E-Commerce (Order Management)
- An Order entity often acts as the Context. It moves through states:
Cart->Checkout->Paid->Shipped. You cannotcancel()aShippedorder, but you cancancel()aPaidone.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Vending Machine | Pure Python | A classic FSM. Idle -> HasCoin -> Dispensing. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Vending Machine FSM
Pattern: State (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
A Vending Machine has 3 states:
1. NoCoinState
2. HasCoinState
3. DispensingState
If you press "Dispense" while in NoCoinState, it errors.
If you insert a coin while in HasCoinState, it refunds the extra coin.
Instead of having massive `if/else` blocks in the VendingMachine,
we put the logic inside the State classes themselves.
KEY INSIGHT:
------------
The State objects have a reference back to the Context (VendingMachine).
This allows them to call `self.machine.set_state(...)` to trigger
transitions.
============================================================
"""
from abc import ABC, abstractmethod
# ─── Context ───────────────────────────────────────────
class VendingMachine:
"""
The Context. It maintains a reference to the current state.
It delegates all work to the state object.
"""
def __init__(self, inventory_count: int) -> None:
self.inventory_count = inventory_count
# Start in the NoCoinState
self._state: State = NoCoinState(self)
def set_state(self, state: 'State') -> None:
"""Called by the States to trigger transitions."""
print(f" 🔄 [Transition] Changing state to: {type(state).__name__}")
self._state = state
# Expose the API to the user, but delegate to the state!
def insert_coin(self) -> None:
self._state.insert_coin()
def eject_coin(self) -> None:
self._state.eject_coin()
def press_button(self) -> None:
self._state.press_button()
def dispense(self) -> None:
self._state.dispense()
# ─── State Interface ───────────────────────────────────
class State(ABC):
"""
Base State class. Every state must know about the Context
so it can trigger transitions.
"""
def __init__(self, machine: VendingMachine) -> None:
self.machine = machine
@abstractmethod
def insert_coin(self) -> None: pass
@abstractmethod
def eject_coin(self) -> None: pass
@abstractmethod
def press_button(self) -> None: pass
@abstractmethod
def dispense(self) -> None: pass
# ─── Concrete States ───────────────────────────────────
class NoCoinState(State):
def insert_coin(self) -> None:
print(" 🪙 Coin inserted.")
# TRANSITION!
self.machine.set_state(HasCoinState(self.machine))
def eject_coin(self) -> None:
print(" ❌ Cannot eject. You haven't inserted a coin.")
def press_button(self) -> None:
print(" ❌ Cannot dispense. Insert a coin first.")
def dispense(self) -> None:
print(" ❌ Pay first!")
class HasCoinState(State):
def insert_coin(self) -> None:
print(" ❌ You already inserted a coin. Returning extra coin.")
def eject_coin(self) -> None:
print(" 🪙 Coin returned.")
# TRANSITION!
self.machine.set_state(NoCoinState(self.machine))
def press_button(self) -> None:
print(" 🟩 Button pressed...")
if self.machine.inventory_count > 0:
# TRANSITION!
self.machine.set_state(DispensingState(self.machine))
# Trigger the internal dispense action immediately
self.machine.dispense()
else:
print(" ❌ Out of stock! Returning coin.")
self.machine.set_state(NoCoinState(self.machine))
def dispense(self) -> None:
print(" ❌ Press the button first.")
class DispensingState(State):
def insert_coin(self) -> None:
print(" ❌ Please wait, already dispensing.")
def eject_coin(self) -> None:
print(" ❌ Sorry, you already pressed the button.")
def press_button(self) -> None:
print(" ❌ Already dispensing!")
def dispense(self) -> None:
print(" 🍬 Dispensing candy!")
self.machine.inventory_count -= 1
print(f" [Inventory: {self.machine.inventory_count} left]")
# TRANSITION! (Back to start)
self.machine.set_state(NoCoinState(self.machine))
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("STATE PATTERN - Vending Machine")
print("=" * 60)
machine = VendingMachine(inventory_count=2)
print("\n--- Scenario 1: Successful Purchase ---")
machine.insert_coin()
machine.press_button()
print("\n--- Scenario 2: Try to break it ---")
machine.press_button() # No coin!
machine.insert_coin()
machine.insert_coin() # Double coin!
machine.eject_coin() # Changed mind
print("\n--- Scenario 3: Buy last item & Out of stock ---")
machine.insert_coin()
machine.press_button() # Buys the 2nd (last) item
machine.insert_coin()
machine.press_button() # Fails gracefully!
print("\n✅ State pattern demonstrated successfully!")
Pattern 20: Strategy (Behavioral)
🧒 The 5-Year-Old Explanation
You need to get to the airport. You have three choices (strategies):
- Take a taxi (expensive, fast).
- Take a bus (cheap, slow).
- Walk (free, very slow).
📦 Prerequisites
You should be comfortable with:
- Dependency Injection (passing an object into another object's constructor).
- First-class functions (Python).
- Interfaces / Protocols.
🔍 How to Spot This Pattern
Think of Strategy when you see:
- You have a lot of similar classes that only differ in the way they execute some behavior (e.g.,
QuickSort,MergeSort,BubbleSort). - You have massive
if/elif/elifblocks where each block contains a complex algorithm to achieve the exact same goal (e.g.,if paymenttype == "paypal": ... elif paymenttype == "stripe": ...). - You want to swap algorithms at runtime without modifying the client code.
- The object fundamentally changes its status and controls its own transitions → State (Ch 19).
- The algorithm never changes; there is only one way to do it.
🧩 The Core Idea
Intent (GoF)
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Context |
Class | Maintains a reference to one of the Strategy objects. Does not know how the strategy executes, just calls the interface. |
Strategy |
Protocol / Callable |
Interface common to all supported algorithms. |
ConcreteStrategy |
Class / Function | Implements the algorithm using the Strategy interface. |
🐍 Python-Specific Implementation
In Java/C++, Strategies are always full classes. In Python, because functions are objects, a Strategy can often just be a function. If your Strategy only has one method (execute()), don't write a class. Just pass a function!
# The "Class" way
class StripePayment:
def pay(self, amount): print(f"Stripe {amount}")
# The "Pythonic" way
def stripe_payment(amount): print(f"Stripe {amount}")
cart = ShoppingCart(strategy=stripe_payment)
🔄 Without Pattern → With Pattern
Level 1: The Monolith
# BAD: The Context is huge, fragile, and violates OCP.
class Navigation:
def build_route(self, a, b, mode):
if mode == "walking":
# 50 lines of walking logic
elif mode == "driving":
# 50 lines of driving logic
elif mode == "transit":
# 50 lines of transit logic
Level 3: Strategy Pattern
# GOOD: Context is completely ignorant of routing algorithms.
class Navigation:
def __init__(self, strategy: RouteStrategy):
self.strategy = strategy
def build_route(self, a, b):
return self.strategy.build(a, b)
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low - it's basic Dependency Injection. |
| SOLID | OCP (add new strategies without modifying Context), SRP (extracts algorithms into separate classes). |
| When Justified | Dealing with different variations of an algorithm (payment gateways, sorting algorithms, compression types). |
🌍 Real-World Framework Sightings
Python Standard Library
- The
sorted()function takes akeyargument. Thatkeyis a Strategy! You pass a function that tellssorted()exactly how to extract a comparison key from each element.sorted(items, key=lambda x: x.age)
Django
- Password hashing. Django has a Context that handles authentication, but you can configure different password hashing strategies (
PBKDF2,Argon2,BCrypt) in the settings.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | E-Commerce Payment Gateways | Pure Python | Swapping out payment strategies (Classes) at runtime. |
| 2 | Pythonic Sorting | Pure Python | Using first-class functions as strategies. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Payment Gateways & Pythonic Strategies
Pattern: Strategy (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have an E-Commerce checkout system. Customers can pay via
Credit Card, PayPal, or Crypto.
Instead of putting all that API logic inside the `ShoppingCart`
class, we inject a Payment Strategy into the cart.
We will show TWO ways to do this in Python:
1. The classic OOP way (Classes)
2. The modern Pythonic way (Functions)
KEY INSIGHT:
------------
The `ShoppingCart` (Context) NEVER changes, no matter how many
new payment methods we invent. It perfectly obeys the Open/Closed Principle.
============================================================
"""
from typing import Callable, Protocol
# ─── Method 1: Classic OOP Strategies ──────────────────
class PaymentStrategy(Protocol):
"""The interface for class-based strategies."""
def pay(self, amount: float) -> None: ...
class StripeStrategy:
def __init__(self, api_key: str) -> None:
self.api_key = api_key
def pay(self, amount: float) -> None:
print(f" 💳 [Stripe] Processing ${amount:.2f} using API key ending in '...{self.api_key[-4:]}'")
class PayPalStrategy:
def __init__(self, email: str) -> None:
self.email = email
def pay(self, amount: float) -> None:
print(f" 🅿️ [PayPal] Processing ${amount:.2f} for account '{self.email}'")
# ─── Method 2: Pythonic Functional Strategies ──────────
# A strategy can just be a function signature!
FunctionalPaymentStrategy = Callable[[float], None]
def crypto_strategy(amount: float) -> None:
btc_amount = amount / 60000.0
print(f" 🪙 [Crypto] Processing {btc_amount:.6f} BTC to wallet '0x1A2B...'")
# ─── The Context ───────────────────────────────────────
class ShoppingCart:
"""
The Context.
It takes a strategy (either a class instance or a function)
and executes it.
"""
def __init__(self) -> None:
self.items: list[dict] = []
self._strategy: PaymentStrategy | FunctionalPaymentStrategy | None = None
def add_item(self, name: str, price: float) -> None:
self.items.append({"name": name, "price": price})
def set_strategy(self, strategy: PaymentStrategy | FunctionalPaymentStrategy) -> None:
"""Swap the strategy at runtime!"""
self._strategy = strategy
def checkout(self) -> None:
if not self._strategy:
print(" ❌ Cannot checkout: No payment strategy selected!")
return
total = sum(item["price"] for item in self.items)
print(f"\n🛒 Checking out {len(self.items)} items. Total: ${total:.2f}")
# Execute the strategy! (Handles both classes and functions)
if callable(getattr(self._strategy, 'pay', None)):
self._strategy.pay(total) # OOP Class
else:
self._strategy(total) # Function
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("STRATEGY PATTERN - Payment Gateways")
print("=" * 60)
cart = ShoppingCart()
cart.add_item("Mechanical Keyboard", 150.00)
cart.add_item("Coffee Mug", 20.00)
# 1. Use Stripe
print("\n--- Paying with Stripe ---")
stripe = StripeStrategy("sk_live_123456789")
cart.set_strategy(stripe)
cart.checkout()
# 2. Swap to PayPal AT RUNTIME
print("\n--- Swapping to PayPal ---")
paypal = PayPalStrategy("alice@example.com")
cart.set_strategy(paypal)
cart.checkout()
# 3. Use the Pythonic Functional Strategy
print("\n--- Swapping to Crypto (Function) ---")
cart.set_strategy(crypto_strategy)
cart.checkout()
print("\n✅ Strategy pattern demonstrated successfully!")
Pattern 21: Template Method (Behavioral)
🧒 The 5-Year-Old Explanation
Think about baking a cake. Every cake follows the exact same steps:
- Mix the dry ingredients.
- Mix the wet ingredients.
- Bake for 30 minutes.
- Add frosting.
The Template Method puts the skeleton in a Base Class, and lets Subclasses fill in the specific details without changing the recipe's order.
📦 Prerequisites
You should be comfortable with:
- Inheritance and Abstract Base Classes (
ABC). - The Hollywood Principle ("Don't call us, we'll call you").
- The difference between
abstractmethodand concrete methods in Python.
🔍 How to Spot This Pattern
Think of Template Method when you see:
- Multiple classes have the exact same sequence of steps, but different implementations for some of those steps.
- You want to enforce an algorithm's structure, but let clients override specific parts.
- You see code duplication across subclasses that can be factored up into a superclass.
- The algorithm's structure changes completely between implementations.
- You want to swap out the entire algorithm at runtime without subclassing → Strategy (Ch 20).
🧩 The Core Idea
Intent (GoF)
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
AbstractClass |
ABC |
Defines abstract primitive operations that concrete subclasses define. Implements the template method defining the skeleton of an algorithm. |
ConcreteClass |
Class | Implements the primitive operations to carry out subclass-specific steps of the algorithm. |
⚖️ Template Method vs Strategy
Both deal with algorithms, but they approach it fundamentally differently.
| Feature | Template Method | Strategy |
|---|---|---|
| Mechanism | Inheritance (Subclassing) | Composition (Dependency Injection) |
| Granularity | Changes parts of an algorithm | Changes the entire algorithm |
| Level | Class level (Static at compile time) | Object level (Dynamic at runtime) |
| Control Flow | Base class calls subclass methods (Hollywood Principle) | Client calls the Strategy |
🪝 Hooks vs Abstract Methods
In a Template Method, you will encounter two types of steps for subclasses to implement:
- Abstract Methods (
@abstractmethod): Subclasses MUST implement these. If they don't, Python will throw an error when instantiating the class. - Hooks: Methods with an empty or default implementation in the base class. Subclasses MAY override them, but they don't have to. Hooks are often placed before or after crucial steps to allow subclasses to inject custom behavior (like
beforesave()oraftersave()).
🔄 Without Pattern → With Pattern
Level 1: Duplicated Boilerplate
# BAD: The structural boilerplate (connect, read, close) is duplicated everywhere.
class PDFMiner:
def extract(self):
file = open()
data = parse_pdf(file) # Only this line changes
file.close()
class CSVMiner:
def extract(self):
file = open()
data = parse_csv(file) # Only this line changes
file.close()
Level 3: Template Method
# GOOD: The Base Class handles the boilerplate. Subclasses handle the details.
class DataMiner(ABC):
def extract(self):
self.open()
self.parse() # Abstract method
self.close()
class PDFMiner(DataMiner):
def parse(self): ... # Just implement the details
🌍 Real-World Framework Sightings
Django / Django REST Framework
- Django's Class-Based Views (CBVs) are entirely built on the Template Method pattern. The base
Viewclass has adispatch()method that acts as the template. Subclasses implement specific steps likeget(),post(),getqueryset(), orgetcontext_data().
Unit Testing (unittest module)
- When you write tests, you inherit from
unittest.TestCase. The framework runssetUp(), thentest_something(), thentearDown().setUpandtearDownare Hooks provided by the Template Method in the test runner.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Data Mining / ETL | Pure Python | Using Abstract methods for required steps and Hooks for optional behavior. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Data Miner / ETL Pipeline
Pattern: Template Method (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You are building an ETL (Extract, Transform, Load) pipeline that
reads data from different sources (CSV files, APIs), processes it,
and saves it to a database.
The sequence of steps is ALWAYS the same:
1. Extract data.
2. Clean data.
3. [Optional hook: Send notification].
4. Load into database.
By putting this sequence in a Base Class (`mine_data`), we guarantee
every data miner follows the exact same protocol.
KEY INSIGHT:
------------
Notice the Hollywood Principle: "Don't call us, we'll call you."
The `DataMiner` base class is in charge. It calls the subclass
methods when it needs them. The subclass never calls the base class.
============================================================
"""
from abc import ABC, abstractmethod
# ─── Abstract Class (The Skeleton) ─────────────────────
class DataMiner(ABC):
"""
The Base Class. It defines the Template Method and the primitive
steps that subclasses must/can implement.
"""
def mine_data(self, source: str) -> None:
"""
THE TEMPLATE METHOD.
This defines the skeleton of the algorithm. It is usually
NOT overridden by subclasses (in languages with 'final', this
method would be marked final).
"""
print(f"\n--- Starting Data Mining for: {source} ---")
# Step 1: Extract (Required)
raw_data = self.extract(source)
# Step 2: Clean (Required)
clean_data = self.clean(raw_data)
# Step 3: Hook (Optional)
self.before_load_hook(clean_data)
# Step 4: Load (Standard implementation in base class)
self._load_to_database(clean_data)
print("--- Mining Complete ---")
# --- Primitive Operations ---
@abstractmethod
def extract(self, source: str) -> str:
"""Subclasses MUST implement how they extract data."""
pass
@abstractmethod
def clean(self, raw_data: str) -> str:
"""Subclasses MUST implement how they clean data."""
pass
def before_load_hook(self, clean_data: str) -> None:
"""
A HOOK.
It has an empty default implementation. Subclasses CAN override
it if they want to inject behavior before saving to the DB.
"""
pass
def _load_to_database(self, data: str) -> None:
"""
A base operation.
It is fully implemented in the base class. Subclasses generally
use this as-is and do not override it.
"""
print(f" 💾 [Base] Saving '{data}' to central database...")
# ─── Concrete Classes (The Details) ────────────────────
class CSVMiner(DataMiner):
"""Mines data from CSV files."""
def extract(self, source: str) -> str:
print(f" 📄 [CSV] Opening CSV file {source} and reading rows...")
return "row1,row2,row3"
def clean(self, raw_data: str) -> str:
print(" 🧹 [CSV] Removing commas and bad rows...")
return "clean_csv_data"
class APIMiner(DataMiner):
"""Mines data from a JSON API."""
def extract(self, source: str) -> str:
print(f" 🌐 [API] Making HTTP GET request to {source}...")
return '{"data": "json_blob"}'
def clean(self, raw_data: str) -> str:
print(" 🧹 [API] Parsing JSON and validating schema...")
return "clean_json_data"
def before_load_hook(self, clean_data: str) -> None:
"""
Overriding the hook! We only want to notify slack for API data.
"""
print(" 🪝 [API Hook] Sending Slack notification about new API data!")
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("TEMPLATE METHOD PATTERN - ETL Pipeline")
print("=" * 60)
# We instantiate the subclasses, but we call the Template Method
# inherited from the Base class!
csv_miner = CSVMiner()
csv_miner.mine_data("users_export.csv")
api_miner = APIMiner()
api_miner.mine_data("https://api.example.com/v1/users")
print("\n✅ Template Method pattern demonstrated successfully!")
Pattern 22: Visitor (Behavioral)
🧒 The 5-Year-Old Explanation
Imagine you are an Insurance Agent. You have to visit three buildings: a House, a Bank, and a Factory. For the House, you check the fire alarms. For the Bank, you check the vault doors. For the Factory, you check the safety goggles.
Instead of writing sell_insurance() inside the House class, the Bank class, and the Factory class (which ruins their clean code), you put all that logic into the InsuranceAgent (the Visitor). The buildings just open their doors and let the agent in.
📦 Prerequisites
You should be comfortable with:
- Single vs Double Dispatch.
- Method overloading (and why Python doesn't have it natively).
- The
functools.singledispatchmodule in Python.
🔍 How to Spot This Pattern
Think of Visitor when you see:
- You need to perform an operation on all elements of a complex object structure (like a Tree or an AST).
- The classes you are operating on rarely change, but the operations you want to perform on them change frequently.
- You want to clean up business logic by extracting operations (like
exportxml(),draw(),calculatetax()) out of the data classes and into a dedicated class.
- The object hierarchy changes frequently (adding new node types). If you add a new node type, you have to update every single Visitor!
- You only have one operation to perform on a flat list of objects. (Just use a standard function and
isinstance()checks).
🧩 The Core Idea
Intent (GoF)
Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
UML Class Diagram
Double Dispatch (The Magic Trick)
In statically typed languages (Java/C++), Visitor relies on Double Dispatch.
- First Dispatch: The Client calls
element.accept(visitor). (Determines the concrete Element type). - Second Dispatch: The Element calls
visitor.visit(self). (Determines the concrete Visitor type AND resolves method overloading based onself).
🐍 Python-Specific Implementation
Python does not support method overloading by type. You cannot write: __CODEPLACEHOLDER0__ The second definition will just overwrite the first!
Two ways to do it in Python:
- The Classic GoF Way (Naming Convention):
- The Modern Pythonic Way (
@singledispatch):
🔄 Without Pattern → With Pattern
Level 1: Fat Data Classes
# BAD: Data classes are bloated with unrelated business logic
class House:
def calculate_insurance(self): ...
def export_to_xml(self): ...
def calculate_tax(self): ...
Level 3: Visitor Pattern
# GOOD: Data classes are pure. Behaviors are extracted.
class House:
def accept(self, visitor):
visitor.visit_house(self)
class InsuranceVisitor:
def visit_house(self, house): ...
class TaxVisitor:
def visit_house(self, house): ...
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | High - Double Dispatch is notoriously confusing. |
| SOLID | SRP (moves operations into dedicated classes), OCP (you can add new behaviors/visitors without modifying data classes). |
| When Justified | AST (Abstract Syntax Tree) traversal, compilers, static analysis tools, exporting object structures to various formats (XML, JSON, PDF). |
| Risk | Adding a new Element class requires modifying the Visitor interface and EVERY concrete Visitor. |
🌍 Real-World Framework Sightings
Python Standard Library (ast module)
- Python's built-in
ast(Abstract Syntax Tree) module uses the Visitor pattern. When you parse Python code into an AST, you can write a class inheriting fromast.NodeVisitorand define methods likevisit_FunctionDef(self, node)to inspect the code structure!
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Exporting Shapes (Classic GoF) | Pure Python | The classic Double-Dispatch implementation with accept(). |
| 2 | Pythonic Single Dispatch | Pure Python | Erasing boilerplate using @singledispatch. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Shape Exporter (Classic GoF vs SingleDispatch)
Pattern: Visitor (Behavioral)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have a drawing app with `Dot`, `Circle`, and `Rectangle` objects.
You need to add a feature to export these shapes to XML. Later, you
need to export them to JSON.
Instead of adding `export_xml()` and `export_json()` to every shape
(which bloats the shape classes), we create a `Visitor` that extracts
the data from the shapes.
KEY INSIGHT:
------------
We will show TWO methods:
1. The Classic GoF approach (Double Dispatch via `accept()`).
2. The modern Pythonic approach (using `@singledispatch`).
============================================================
"""
from typing import Protocol
from functools import singledispatch
# ==========================================================
# METHOD 1: Classic GoF (Double Dispatch)
# ==========================================================
print("=" * 60)
print("VISITOR PATTERN - Classic GoF")
print("=" * 60)
# ─── Elements ──────────────────────────────────────────
class Shape(Protocol):
def accept(self, visitor: 'Visitor') -> str: ...
class Dot:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def accept(self, visitor: 'Visitor') -> str:
# Double Dispatch Step 2: The Element calls the specific
# visitor method associated with its class.
return visitor.visit_dot(self)
class Circle:
def __init__(self, radius: int) -> None:
self.radius = radius
def accept(self, visitor: 'Visitor') -> str:
return visitor.visit_circle(self)
# ─── Visitor Interface & Implementations ───────────────
class Visitor(Protocol):
def visit_dot(self, dot: Dot) -> str: ...
def visit_circle(self, circle: Circle) -> str: ...
class XMLExporterVisitor:
"""Extracts state from shapes and returns XML."""
def visit_dot(self, dot: Dot) -> str:
return f"<dot><x>{dot.x}</x><y>{dot.y}</y></dot>"
def visit_circle(self, circle: Circle) -> str:
return f"<circle><radius>{circle.radius}</radius></circle>"
# ─── Classic Demo ───────────────
shapes: list[Shape] = [Dot(10, 20), Circle(15)]
xml_visitor = XMLExporterVisitor()
for shape in shapes:
# Double Dispatch Step 1: The Client calls accept() on the Element
result = shape.accept(xml_visitor)
print(f" Exported: {result}")
# ==========================================================
# METHOD 2: Modern Python (SingleDispatch)
# ==========================================================
print("\n" + "=" * 60)
print("VISITOR PATTERN - Modern Python (@singledispatch)")
print("=" * 60)
# ─── Elements (Notice there is NO accept() method!) ────
class PythonicDot:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
class PythonicCircle:
def __init__(self, radius: int) -> None:
self.radius = radius
# ─── Visitor using SingleDispatch ──────────────────────
# @singledispatch gives us method overloading by type!
@singledispatch
def export_json(shape) -> str:
raise NotImplementedError("Unsupported shape type")
@export_json.register(PythonicDot)
def _(dot: PythonicDot) -> str:
return f'{{"type": "dot", "x": {dot.x}, "y": {dot.y}}}'
@export_json.register(PythonicCircle)
def _(circle: PythonicCircle) -> str:
return f'{{"type": "circle", "radius": {circle.radius}}}'
# ─── Modern Demo ───────────────
modern_shapes = [PythonicDot(30, 40), PythonicCircle(50)]
for shape in modern_shapes:
# Look how clean this is! No accept() boilerplate.
# Python automatically routes it to the correct registered function.
result = export_json(shape)
print(f" Exported: {result}")
print("\n✅ Visitor pattern demonstrated successfully!")
Pattern 23: Repository (Architectural)
🧒 The 5-Year-Old Explanation
Imagine you want to read a book. You don't care if the book is on a shelf, in a locked glass case, or buried in a box in the basement. You just go to the Librarian and say "Get me Harry Potter." The librarian finds it and hands it to you.
The Repository Pattern is the Librarian. It sits between your business logic (you) and the database (the shelves). Your code just says userrepo.getby_id(5), and the Repository handles the messy SQL, ORM, or API calls to fetch it.
📦 Prerequisites
You should be comfortable with:
- Domain-Driven Design (DDD) concepts (Entities vs DTOs vs DB Models).
- SQL and basic ORM usage (e.g., SQLAlchemy).
- Interface segregation.
🔍 How to Spot This Pattern
Think of Repository when you see:
- You have complex database queries scattered all over your business logic.
- You want to unit test your business logic without actually connecting to a real database (you can mock the repository!).
- You are using Clean Architecture, Hexagonal Architecture, or Domain-Driven Design.
- You are building a very simple CRUD app (Create, Read, Update, Delete) where the business logic is the database logic. (Just use Django/SQLAlchemy Active Record directly).
🧩 The Core Idea
Intent (Fowler / Evans)
Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
Repository Interface |
Protocol / ABC |
Defines the standard contract (e.g., add(), get()). Keeps domain logic completely ignorant of the database technology. |
Concrete Repository |
Class | Implements the interface using a specific technology (SQLAlchemy, PyMongo, raw SQL). |
Domain Entity |
Class / Dataclass | The pure Python object that holds business data and rules. It has NO database connections. |
DB Model |
Class | (Optional) The ORM class mapping to the database table. The repository converts between DB Models and Domain Entities. |
🔄 Without Pattern → With Pattern
Level 1: Active Record (Tightly Coupled)
# BAD (for complex apps): Business logic is heavily tied to the DB ORM.
# If you want to test this, you MUST spin up a real database.
def apply_discount(user_id):
# This is an SQLAlchemy ORM call
user = UserDBModel.query.filter_by(id=user_id).first()
user.balance -= 10
db.session.commit()
Level 3: Repository Pattern (Decoupled)
# GOOD: Business logic only talks to the pure interface.
def apply_discount(user_id, repo: UserRepository):
user = repo.get(user_id) # Returns a pure Domain Entity
user.apply_discount(10)
repo.update(user)
# Testing is trivial! Just pass a Fake Repository in memory.
fake_repo = InMemoryUserRepository({1: User(id=1, balance=50)})
apply_discount(1, fake_repo)
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | High - requires creating Interfaces, Repositories, Domain Entities, and DB Models (and mapping between them). |
| SOLID | DIP (Business logic depends on abstraction, not database details). SRP (Separates DB queries from business rules). |
| When Justified | Enterprise applications, highly testable systems, Domain-Driven Design. |
| When Over-engineered | Standard web apps. Frameworks like Django are built around Active Record (User.objects.get()). Forcing a Repository pattern on top of Django's ORM is usually a massive anti-pattern that fights the framework. |
🌍 Real-World Framework Sightings
.NET / Java Spring
- The Repository pattern is the standard, default way of accessing databases in enterprise C# and Java applications.
Python (SQLAlchemy 2.0)
- While SQLAlchemy provides Active Record capabilities, its
Sessionobject combined with explicit mapping allows you to build perfect DDD Repositories natively.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Testable User Service | Pure Python | Decoupling business logic from a simulated database using an Abstract Repository and an In-Memory fake. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Testable User Service with Repository
Pattern: Repository (Architectural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have a service that upgrades a user to a "Premium" account.
To do this, it needs to check their balance, deduct funds, and
update their status.
If you hardcode database queries (SQL/ORM) into this service,
you cannot unit test it without a real database.
Instead, we use a `UserRepository` interface. For production, we
use a `SqlUserRepository`. For testing, we use a `FakeUserRepository`.
KEY INSIGHT:
------------
The `UserService` (Business Logic) knows absolutely NOTHING about SQL,
sessions, connections, or database tables. It only knows about the
pure `User` entity and the `UserRepository` protocol.
============================================================
"""
from typing import Protocol, Optional
from dataclasses import dataclass
# ─── 1. Domain Entity (Pure Python, No DB logic) ────────
@dataclass
class User:
"""A pure domain entity. Contains business logic and data."""
id: int
name: str
balance: float
is_premium: bool = False
def can_afford(self, amount: float) -> bool:
return self.balance >= amount
def upgrade_to_premium(self) -> None:
if self.is_premium:
raise ValueError("User is already premium!")
self.is_premium = True
self.balance -= 100.0
# ─── 2. Repository Interface ───────────────────────────
class UserRepository(Protocol):
"""The contract for data access. Completely independent of the DB."""
def get(self, user_id: int) -> Optional[User]: ...
def save(self, user: User) -> None: ...
# ─── 3. Business Service (The Use Case) ────────────────
class UserService:
"""
Business Logic Layer.
Notice how it takes the repository via Dependency Injection.
"""
def __init__(self, repo: UserRepository) -> None:
self.repo = repo
def upgrade_user(self, user_id: int) -> None:
print(f" [Service] Attempting to upgrade user {user_id}...")
user = self.repo.get(user_id)
if not user:
print(" ❌ Upgrade failed: User not found.")
return
if not user.can_afford(100.0):
print(f" ❌ Upgrade failed: {user.name} has insufficient funds (${user.balance:.2f}).")
return
# Execute business logic on the pure entity
user.upgrade_to_premium()
# Persist the changes
self.repo.save(user)
print(f" ✅ Upgrade successful! {user.name} is now Premium. Balance: ${user.balance:.2f}")
# ─── 4. Concrete Repositories ──────────────────────────
class SqlUserRepository:
"""
Production Repository.
Pretend this connects to Postgres using SQLAlchemy.
"""
def __init__(self, db_connection_string: str) -> None:
print(f" [DB] Connecting to Production Database at {db_connection_string}...")
def get(self, user_id: int) -> Optional[User]:
print(f" [DB] Executing: SELECT * FROM users WHERE id = {user_id}")
# Simulated DB fetch...
return User(id=user_id, name="Production Proderson", balance=500.0)
def save(self, user: User) -> None:
print(f" [DB] Executing: UPDATE users SET balance={user.balance}, is_premium={user.is_premium} WHERE id={user.id}")
class FakeUserRepository:
"""
Testing Repository.
Stores data in memory (a dictionary). Lightning fast. No DB required.
"""
def __init__(self, initial_data: list[User]) -> None:
self._store = {user.id: user for user in initial_data}
def get(self, user_id: int) -> Optional[User]:
print(f" [TestRepo] Fetching user {user_id} from memory...")
return self._store.get(user_id)
def save(self, user: User) -> None:
print(f" [TestRepo] Saving user {user.id} to memory...")
self._store[user.id] = user
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("REPOSITORY PATTERN")
print("=" * 60)
print("\n--- 1. Running in Production Mode ---")
prod_repo = SqlUserRepository("postgresql://user:pass@localhost/db")
prod_service = UserService(prod_repo)
prod_service.upgrade_user(99)
print("\n--- 2. Running in Unit Test Mode ---")
# We can easily setup edge cases without touching a database!
test_repo = FakeUserRepository([
User(id=1, name="Alice (Rich)", balance=500.0),
User(id=2, name="Bob (Poor)", balance=20.0)
])
test_service = UserService(test_repo)
print("\n [Testing Bob (Should Fail)]")
test_service.upgrade_user(2)
print("\n [Testing Alice (Should Succeed)]")
test_service.upgrade_user(1)
print("\n✅ Repository pattern demonstrated successfully!")
Pattern 24: Unit of Work (Architectural)
🧒 The 5-Year-Old Explanation
Imagine you are at the grocery store. You pick up milk, bread, and eggs. You don't walk to the cashier, pay for the milk, then walk back, get the bread, walk to the cashier, pay for the bread... that would be exhausting!
Instead, you put everything in a shopping cart. When you are completely done, you push the cart to the cashier and pay for everything in one single transaction.
The Unit of Work (UoW) pattern is the shopping cart for your database. It keeps track of everything you changed, and saves it all at once at the very end.
📦 Prerequisites
You should be comfortable with:
- Database Transactions (BEGIN, COMMIT, ROLLBACK).
- The Repository Pattern (Ch 23). UoW and Repository are almost always used together.
- Python Context Managers (
withblocks).
🔍 How to Spot This Pattern
Think of Unit of Work when you see:
- You need to update multiple different tables/repositories, and if one fails, they ALL need to fail (Atomicity).
- You are making too many small trips to the database (N+1 problem on writes).
- You see
db.session.commit()scattered randomly throughout your business logic, leading to partial failures.
- You are building a NoSQL app where documents are strictly isolated and cross-document transactions aren't supported.
🧩 The Core Idea
Intent (Fowler)
Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.
UML Class Diagram
UML Sequence Diagram
🔧 Pattern Anatomy
Participants
| Participant | Python Type | Role |
|---|---|---|
UnitOfWork |
Context Manager | Defines a block of code where all database interactions form a single atomic transaction. Exposes Repositories to the client. |
Repositories |
Interface | Handled by the UoW. The UoW injects the shared database session into the repositories. |
🔄 Without Pattern → With Pattern
Level 1: Scattered Commits (Partial Failure Risk)
# BAD: If the email server crashes, the user is charged but gets no receipt,
# and the inventory isn't updated!
def checkout(user_id, cart_id):
user_repo.deduct_funds(user_id, 100)
db.commit() # DANGER!
cart_repo.clear(cart_id)
db.commit() # DANGER!
send_email(user_id) # Crashes here!
inventory_repo.update()
db.commit()
Level 3: Unit of Work
# GOOD: Everything succeeds, or everything fails cleanly.
def checkout(user_id, cart_id, uow: AbstractUnitOfWork):
with uow: # Starts transaction
uow.users.deduct_funds(user_id, 100)
uow.carts.clear(cart_id)
uow.inventory.update()
send_email(user_id) # If this crashes, __exit__ triggers ROLLBACK automatically.
uow.commit() # Only hit if everything above succeeds.
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | High - requires wrapping all database interactions inside Context Managers. |
| SOLID | SRP (Business logic handles rules, UoW handles transactions). |
| When Justified | Applications requiring strict data integrity across multiple tables/repositories. |
🌍 Real-World Framework Sightings
SQLAlchemy
- The SQLAlchemy
Sessionobject is literally a Unit of Work. When youadd(),delete(), and modify objects, nothing hits the database until you callsession.commit().
Django
from django.db import transaction->with transaction.atomic():. This is Django's built-in Unit of Work context manager.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Bank Transfer | Pure Python | Moving money between accounts ensuring Atomicity using Python Context Managers. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Bank Transfer with Unit of Work
Pattern: Unit of Work (Architectural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You need to transfer $500 from Alice's account to Bob's account.
1. Deduct $500 from Alice.
2. Add $500 to Bob.
If step 1 succeeds but step 2 crashes (e.g. database disconnects),
the money is destroyed! It vanished into the void.
To fix this, we use a Unit of Work (UoW). The UoW ensures that BOTH
operations happen together (Atomicity). If anything fails, it rolls
back all changes.
KEY INSIGHT:
------------
We use Python Context Managers (`__enter__` and `__exit__`).
If an exception occurs inside the `with` block, `__exit__` catches
it and calls `rollback()`.
============================================================
"""
from abc import ABC, abstractmethod
from types import TracebackType
# ─── Simulated Database ────────────────────────────────
class FakeDatabaseSession:
"""Simulates an ORM session (like SQLAlchemy's Session)."""
def __init__(self) -> None:
self.accounts = {"Alice": 1000, "Bob": 100}
self.uncommitted_changes: dict[str, int] = {}
self.is_active = False
def begin(self) -> None:
self.is_active = True
self.uncommitted_changes.clear()
print(" [DB] BEGIN TRANSACTION")
def commit(self) -> None:
if not self.is_active: return
self.accounts.update(self.uncommitted_changes)
self.is_active = False
self.uncommitted_changes.clear()
print(" [DB] COMMIT")
def rollback(self) -> None:
if not self.is_active: return
self.is_active = False
self.uncommitted_changes.clear()
print(" [DB] ROLLBACK (Discarding pending changes)")
def update_balance(self, user: str, new_balance: int) -> None:
self.uncommitted_changes[user] = new_balance
# ─── Repositories ──────────────────────────────────────
class AccountRepository:
"""Takes a session and uses it to execute queries."""
def __init__(self, session: FakeDatabaseSession) -> None:
self.session = session
def get_balance(self, user: str) -> int:
# Check uncommitted changes first, then committed state
return self.session.uncommitted_changes.get(user, self.session.accounts[user])
def set_balance(self, user: str, balance: int) -> None:
print(f" [Repo] Setting {user}'s balance to ${balance}")
self.session.update_balance(user, balance)
# ─── Unit Of Work ──────────────────────────────────────
class AbstractUnitOfWork(ABC):
accounts: AccountRepository
def __enter__(self) -> 'AbstractUnitOfWork':
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None
) -> None:
"""Called automatically when the 'with' block ends."""
if exc_type:
self.rollback() # An error happened! Rollback!
@abstractmethod
def commit(self) -> None: ...
@abstractmethod
def rollback(self) -> None: ...
class DatabaseUnitOfWork(AbstractUnitOfWork):
def __init__(self, session: FakeDatabaseSession) -> None:
self.session = session
def __enter__(self) -> 'DatabaseUnitOfWork':
# 1. Start the transaction
self.session.begin()
# 2. Instantiate repositories injecting the active session
self.accounts = AccountRepository(self.session)
return super().__enter__()
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
super().__exit__(exc_type, exc_val, exc_tb)
# We always rollback at the end. If commit() was called successfully
# inside the block, this rollback does nothing.
self.rollback()
def commit(self) -> None:
self.session.commit()
def rollback(self) -> None:
self.session.rollback()
# ─── Business Logic (The Service) ──────────────────────
def transfer_money(uow: AbstractUnitOfWork, sender: str, receiver: str, amount: int, crash: bool = False) -> None:
print(f"\n--- Initiating Transfer: ${amount} from {sender} to {receiver} ---")
try:
# THE MAGIC HAPPENS HERE
with uow:
# 1. Read balances
sender_bal = uow.accounts.get_balance(sender)
receiver_bal = uow.accounts.get_balance(receiver)
# 2. Modify balances
uow.accounts.set_balance(sender, sender_bal - amount)
# Simulate a system crash right in the middle!
if crash:
print(" 💥 SYSTEM CRASH (Power out, Exception thrown)!")
raise Exception("System crash!")
uow.accounts.set_balance(receiver, receiver_bal + amount)
# 3. Commit only if we reach the end
uow.commit()
print(" ✅ Transfer successful.")
except Exception as e:
print(f" ❌ Transfer failed: {e}")
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("UNIT OF WORK PATTERN - Bank Transfer")
print("=" * 60)
# Setup global database connection
db_session = FakeDatabaseSession()
print("\nInitial State:")
print(f" Alice: ${db_session.accounts['Alice']}")
print(f" Bob: ${db_session.accounts['Bob']}")
# Scenario 1: Successful Transfer
transfer_money(DatabaseUnitOfWork(db_session), "Alice", "Bob", 200)
print("\nState after Scenario 1:")
print(f" Alice: ${db_session.accounts['Alice']} (Expected: 800)")
print(f" Bob: ${db_session.accounts['Bob']} (Expected: 300)")
# Scenario 2: Crashed Transfer (Money should NOT be lost!)
transfer_money(DatabaseUnitOfWork(db_session), "Alice", "Bob", 500, crash=True)
print("\nState after Scenario 2 (Crash):")
print(f" Alice: ${db_session.accounts['Alice']} (Expected: 800 - No change!)")
print(f" Bob: ${db_session.accounts['Bob']} (Expected: 300 - No change!)")
print("\n✅ Unit of Work pattern demonstrated successfully!")
Pattern 25: Dependency Injection (Architectural)
🧒 The 5-Year-Old Explanation
Imagine you are a race car driver. You know how to steer, brake, and accelerate. But you don't build the engine yourself before you drive. You don't build the tires yourself.
The mechanic team builds the engine, puts on the tires, and then gives (injects) the finished car to you.
In code, Dependency Injection (DI) means an object should not create its own tools (dependencies). It should be given those tools from the outside.
📦 Prerequisites
You should be comfortable with:
- Object instantiation vs Interface implementation.
- The
SOLIDprinciples, specifically Dependency Inversion Principle (DIP). - Type hinting (
Protocol/ABC).
🔍 How to Spot This Pattern
Think of Dependency Injection when you see:
- You are hardcoding
Database()orEmailClient()directly inside your class'sinitmethod. - You want to write unit tests, but your class is connecting to a real production database because you can't easily swap out the connection.
- You see massive configuration blocks scattered across multiple files.
- The dependency is a built-in Python primitive (like
dictorlist), or a simple dataclass that has no side effects (likeUser(name="Bob")). You don't need to inject DTOs or Value Objects.
🧩 The Core Idea
Intent
Separate the creation of a client's dependencies from its own behavior. This allows the program design to be loosely coupled and to follow the Dependency Inversion and Single Responsibility principles.
The Dependency Inversion Principle (DIP)
- High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
Types of Injection
- Constructor Injection (Most Common & Recommended): Dependencies are passed into the
_init_method. - Method Injection: Dependencies are passed into a specific method when called (e.g.,
def calculate(self, tax_strategy):). - Property/Setter Injection: Dependencies are set via a property (e.g.,
obj.logger = MyLogger()). (Less common, prone to errors if the object is used before the setter is called).
🔄 Without Pattern → With Pattern
Level 1: Hardcoded Dependencies (Tight Coupling)
# BAD: The OrderService creates its own dependencies.
# It is permanently glued to Stripe and Postgres. You cannot unit test this.
class OrderService:
def __init__(self):
self.payment = StripeAPI()
self.db = PostgresDB()
def checkout(self, amount):
self.payment.charge(amount)
self.db.save("order")
Level 3: Constructor Injection (Loose Coupling)
# GOOD: OrderService doesn't care HOW things are built. It just uses them.
class OrderService:
def __init__(self, payment: PaymentProtocol, db: DBProtocol):
self.payment = payment
self.db = db
# The "Mechanic" (or DI Container) wires everything together outside:
test_payment = MockPayment()
test_db = InMemoryDB()
service = OrderService(test_payment, test_db) # Fully testable!
🏗️ DI Containers (Inversion of Control)
In large applications, passing 20 dependencies down a chain of 10 objects becomes a nightmare (this is called Dependency Hell).
A DI Container (or IoC Container) solves this. You tell the container:
- "Whenever someone asks for a
PaymentProtocol, give them aStripeAPI." - "Whenever someone asks for a
DBProtocol, give them aPostgresDB."
OrderService, and it magically builds the StripeAPI, builds the PostgresDB, passes them into the OrderService, and hands you the finished object!
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Low for manual injection. High if building/using complex IoC containers. |
| SOLID | DIP (High level modules depend on abstractions), SRP (Object creation is separated from business logic). |
| Testability | Extremely High. This is the primary mechanism that enables modern unit testing (Mocking). |
🌍 Real-World Framework Sightings
FastAPI
- FastAPI has a built-in Dependency Injection system! Whenever you write
def getuser(db: Session = Depends(getdb)):, you are using FastAPI's DI container. It handles creating the database session and passing it to your function.
Pytest
- Pytest
fixturesare a form of Dependency Injection. You define a fixture, and then you just put its name in your test function signature, and Pytest injects it for you!
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Manual Dependency Injection | Pure Python | Demonstrating Constructor Injection and writing testable code. |
| 2 | Mini DI Container | Pure Python | Building a simple IoC Container to automate object wiring. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Manual DI & Mini DI Container
Pattern: Dependency Injection (Architectural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
You have a `NotificationService` that needs to fetch users from a
database and send them an SMS message.
Instead of hardcoding `PostgresDatabase()` and `TwilioSMSClient()`
inside the service, we use Dependency Injection.
We will show TWO ways to wire this up:
1. Manual wiring (doing it yourself).
2. Using a basic Dependency Injection Container (IoC).
KEY INSIGHT:
------------
The `NotificationService` ONLY relies on Protocols (Interfaces).
It has zero knowledge of Postgres or Twilio.
============================================================
"""
from typing import Protocol, Type, Any, Callable, Dict
# ─── 1. The Abstractions (Protocols) ───────────────────
class DatabaseProtocol(Protocol):
def get_phone_number(self, user_id: int) -> str: ...
class SMSClientProtocol(Protocol):
def send_sms(self, phone: str, message: str) -> None: ...
# ─── 2. The Low-Level Modules (Details) ────────────────
class PostgresDatabase:
"""A concrete implementation of DatabaseProtocol."""
def get_phone_number(self, user_id: int) -> str:
print(" [DB] Connecting to Postgres...")
return f"+1-555-010{user_id}"
class TwilioSMSClient:
"""A concrete implementation of SMSClientProtocol."""
def __init__(self, api_key: str) -> None:
self.api_key = api_key
def send_sms(self, phone: str, message: str) -> None:
print(f" [Twilio] Sending '{message}' to {phone} using key {self.api_key[:4]}...")
# ─── 3. The High-Level Module (Business Logic) ─────────
class NotificationService:
"""
Notice the CONSTRUCTOR INJECTION.
We ask for the Protocols, not the concrete classes!
"""
def __init__(self, db: DatabaseProtocol, sms: SMSClientProtocol) -> None:
self.db = db
self.sms = sms
def notify_user(self, user_id: int, message: str) -> None:
print(f"\n--- Notifying User {user_id} ---")
phone = self.db.get_phone_number(user_id)
self.sms.send_sms(phone, message)
# ==========================================================
# METHOD 1: Manual Wiring
# ==========================================================
print("=" * 60)
print("DEPENDENCY INJECTION - Manual Wiring")
print("=" * 60)
# We act as the "Mechanic". We build the parts, then assemble the car.
db_instance = PostgresDatabase()
sms_instance = TwilioSMSClient("sk_live_987654321")
# We pass the dependencies IN.
service = NotificationService(db=db_instance, sms=sms_instance)
service.notify_user(1, "Your pizza is ready!")
# ==========================================================
# METHOD 2: Mini DI Container (Inversion of Control)
# ==========================================================
# In complex apps, building 50 objects manually is tedious.
# A DI Container automates the wiring.
class DIContainer:
"""A very basic DI Container."""
def __init__(self) -> None:
# Maps a Protocol to a factory function that builds the concrete class
self._providers: Dict[Type, Callable[[], Any]] = {}
def register(self, protocol: Type, factory: Callable[[], Any]) -> None:
"""Register how to build a certain type."""
self._providers[protocol] = factory
def resolve(self, protocol: Type) -> Any:
"""Ask the container for an object, and it builds it for you."""
if protocol not in self._providers:
raise ValueError(f"No provider registered for {protocol}")
return self._providers[protocol]()
print("\n" + "=" * 60)
print("DEPENDENCY INJECTION - IoC Container")
print("=" * 60)
container = DIContainer()
# 1. Configuration Phase: Tell the container how to build things
container.register(DatabaseProtocol, lambda: PostgresDatabase())
container.register(SMSClientProtocol, lambda: TwilioSMSClient("sk_live_987654321"))
# 2. Wire up the Service
# We ask the container to resolve the dependencies for us!
container.register(
NotificationService,
lambda: NotificationService(
db=container.resolve(DatabaseProtocol),
sms=container.resolve(SMSClientProtocol)
)
)
# 3. Execution Phase: Just ask for the finished service!
# (Notice we don't manually build the DB or SMS client here!)
app_service: NotificationService = container.resolve(NotificationService)
app_service.notify_user(2, "Your driver is arriving.")
print("\n✅ Dependency Injection demonstrated successfully!")
Pattern 26: Event Sourcing & CQRS (Architectural)
🧒 The 5-Year-Old Explanation
Normal Database (CRUD): Imagine a bank account. You deposit $100. The bank crosses out your old balance of $0 and writes $100. You deposit $50. They cross out $100 and write $150. If you ask, "How did I get $150?", the bank says, "I don't know, that's just the number on the paper."
Event Sourcing: The bank doesn't write down your balance. Instead, they write down a list of things that happened:
- Opened account
- Deposited $100
- Deposited $50
CQRS: When you ask "What's my balance?" it takes a long time to add up that whole list. So, CQRS splits the bank in half. One half ONLY writes down the events (Event Sourcing). The other half adds up the events in the background and keeps a separate, super-fast chalkboard with just the final number $150 for you to read.
📦 Prerequisites
You should be comfortable with:
- Command Pattern (Ch 14).
- Observer Pattern / PubSub (Ch 18).
- Database scaling concepts (Read Replicas).
🔍 How to Spot This Pattern
Think of Event Sourcing/CQRS when you see:
- You absolutely cannot lose historical data. You need a perfect audit trail of every change ever made (Finance, Healthcare).
- You want to be able to "time travel" your application to see its exact state 3 weeks ago.
- Your application has a massive imbalance between Reads (99%) and Writes (1%).
- You are building a simple CRUD app. Event Sourcing will increase your complexity by 10x. Do not use it unless you truly have business requirements for it.
🧩 The Core Idea
CQRS (Command Query Responsibility Segregation)
Segregate operations that read data (Queries) from operations that update data (Commands) by using separate interfaces, and often entirely separate databases.
Event Sourcing
Capture all changes to an application state as a sequence of events. Do not store the current state; instead, reconstruct it by replaying the events.
Architecture Diagram
🔧 Pattern Anatomy
Participants
| Participant | Role |
|---|---|
Command |
An intent to change state (e.g., DepositMoneyCommand(amount=50)). It is validated before becoming an event. |
Event |
A fact that has already happened (e.g., MoneyDepositedEvent(amount=50)). Events are immutable. They cannot be changed or deleted. |
Event Store |
An append-only database (like Kafka or EventStoreDB) that holds the single source of truth: the stream of events. |
Projection |
A background worker that listens to new events in the Event Store, calculates the new state, and saves the final result to a Read Database. |
Read DB |
A highly optimized database (like Redis or ElasticSearch) that only answers queries. It is completely decoupled from the Event Store. |
🔄 Without Pattern → With Pattern
Level 1: Standard CRUD (Loss of History)
# BAD (for audit systems): The previous state is gone forever!
def change_address(user_id, new_address):
user = db.query(User).get(user_id)
user.address = new_address
db.commit() # The old address is permanently deleted!
Level 3: Event Sourcing + CQRS
# GOOD: We append a fact to history. We do not UPDATE rows.
def change_address(user_id, new_address):
# 1. Create the Event
event = UserMovedEvent(user_id=user_id, new_address=new_address, timestamp=now())
# 2. Append to Event Store (WRITE)
event_store.append(event)
# 3. A background process hears the event and updates the Read DB (CQRS)
# read_db.update_user_address(user_id, new_address)
⏱️ Trade-offs
| Aspect | Assessment |
|---|---|
| Complexity | Extreme. You have to deal with eventual consistency, message brokers, event versions, and complex infrastructure. |
| Auditability | Flawless. You have a cryptographic record of every action. |
| Scalability | Massive. You can scale the Read DB (Queries) completely independently from the Event Store (Commands). |
| Performance Issue | If a user has 10,000 events, reconstructing their state takes a long time. (Mitigated using 'Snapshots' every 100 events). |
🌍 Real-World Framework Sightings
Git
- Git is exactly Event Sourcing! The "events" are your commits. To get the current state of your codebase (the projection), Git replays all the commits from the beginning of time.
Apache Kafka / RabbitMQ
- These messaging systems form the backbone of modern CQRS/Event Sourced microservices.
✅ Examples in This Chapter
| # | Example | Framework | Key Concept |
|---|---|---|---|
| 1 | Event Sourced Bank Account | Pure Python | Separating Commands and Queries, appending events to a stream, and projecting the current balance. |
⚡ Interactive Python 3.14 Implementations (1)
"""
============================================================
Example: Event Sourced Bank Account
Pattern: Event Sourcing & CQRS (Architectural)
Framework: Pure Python
============================================================
REAL-WORLD SCENARIO:
--------------------
We are building a Bank. We cannot use standard CRUD (UPDATE/DELETE).
We must use Event Sourcing to maintain a perfect audit log.
We will build:
1. An Event Store (Append-only log of facts).
2. A Write Side (Processes Commands and creates Events).
3. A Read Side / Projection (Listens to Events and updates the balance).
KEY INSIGHT:
------------
Notice that we NEVER do `balance = new_amount`. We only ever append
events to a list. The current balance is derived by replaying history.
============================================================
"""
from dataclasses import dataclass
from datetime import datetime
from typing import List, Dict, Protocol
# ─── 1. Events (Immutable Facts) ───────────────────────
# Events are named in the PAST TENSE. They have already happened.
@dataclass(frozen=True)
class Event:
account_id: str
timestamp: datetime = datetime.now()
@dataclass(frozen=True)
class AccountOpenedEvent(Event):
initial_deposit: int = 0
@dataclass(frozen=True)
class MoneyDepositedEvent(Event):
amount: int = 0
@dataclass(frozen=True)
class MoneyWithdrawnEvent(Event):
amount: int = 0
class Projection(Protocol):
"""Protocol for Read Models / Projections that consume domain events."""
def handle_event(self, event: Event) -> None:
...
# ─── 2. Event Store (The Single Source of Truth) ───────
class EventStore:
"""An append-only database (like Kafka or EventStoreDB)."""
def __init__(self) -> None:
# Dictionary mapping account_id -> List of Events
self.streams: Dict[str, List[Event]] = {}
# Simple PubSub for the Read Models
self.subscribers: List['Projection'] = []
def append(self, event: Event) -> None:
if event.account_id not in self.streams:
self.streams[event.account_id] = []
self.streams[event.account_id].append(event)
print(f" 💾 [EventStore] Appended: {type(event).__name__}")
# Notify the Read Side (CQRS)
for sub in self.subscribers:
sub.handle_event(event)
def get_stream(self, account_id: str) -> List[Event]:
"""Fetch all history for an account."""
return self.streams.get(account_id, [])
# ─── 3. Write Side / Command API ───────────────────────
# Handles business logic, validates rules, and generates events.
class BankAccountService:
def __init__(self, event_store: EventStore) -> None:
self.event_store = event_store
def open_account(self, account_id: str, deposit: int) -> None:
# Validate Command
if deposit < 0: raise ValueError("Cannot open with negative balance")
# Generate and Save Event
event = AccountOpenedEvent(account_id=account_id, initial_deposit=deposit)
self.event_store.append(event)
def deposit(self, account_id: str, amount: int) -> None:
if amount <= 0: raise ValueError("Deposit must be positive")
event = MoneyDepositedEvent(account_id=account_id, amount=amount)
self.event_store.append(event)
def withdraw(self, account_id: str, amount: int) -> None:
# WARNING: In a real Event Sourced system, we have to rebuild the
# state here to ensure they have enough funds before withdrawing!
# For this simple example, we skip the balance check.
if amount <= 0: raise ValueError("Withdrawal must be positive")
event = MoneyWithdrawnEvent(account_id=account_id, amount=amount)
self.event_store.append(event)
# ─── 4. Read Side / Projection (CQRS) ──────────────────
# A background worker that listens to events and updates a fast DB for queries.
class BalanceProjection:
"""The Read Model. Highly optimized for fast queries."""
def __init__(self) -> None:
# This acts like a Redis cache or a highly indexed SQL table
self.read_database: Dict[str, int] = {}
def handle_event(self, event: Event) -> None:
"""Called automatically when the EventStore receives a new event."""
account_id = event.account_id
# Initialize if not exists
if account_id not in self.read_database:
self.read_database[account_id] = 0
# Apply the changes to the Read DB
if isinstance(event, AccountOpenedEvent):
self.read_database[account_id] += event.initial_deposit
elif isinstance(event, MoneyDepositedEvent):
self.read_database[account_id] += event.amount
elif isinstance(event, MoneyWithdrawnEvent):
self.read_database[account_id] -= event.amount
print(f" 📊 [Projection] Updated Read DB. {account_id} Balance is now ${self.read_database[account_id]}")
def query_balance(self, account_id: str) -> int:
"""Lightning fast query. No calculations needed!"""
return self.read_database.get(account_id, 0)
# ─── 5. Time Travel (Replaying History) ────────────────
# If the Read DB crashes, we can rebuild it from scratch!
def rebuild_state_from_events(events: List[Event]) -> int:
balance = 0
for event in events:
if isinstance(event, AccountOpenedEvent): balance += event.initial_deposit
elif isinstance(event, MoneyDepositedEvent): balance += event.amount
elif isinstance(event, MoneyWithdrawnEvent): balance -= event.amount
return balance
# ─── Demo ──────────────────────────────────────────────
if __name__ == "__main__":
print("=" * 60)
print("EVENT SOURCING & CQRS - Bank Account")
print("=" * 60)
# Setup Infrastructure
store = EventStore()
projection = BalanceProjection()
store.subscribers.append(projection) # Connect the Read Side to the EventStore
# Setup Write API
service = BankAccountService(store)
print("\n--- Processing Commands (Write Side) ---")
service.open_account("ACC_123", 500)
service.deposit("ACC_123", 200)
service.withdraw("ACC_123", 100)
service.deposit("ACC_123", 50)
print("\n--- Processing Queries (Read Side) ---")
# Client asks for balance. It queries the lightning fast Read DB.
final_balance = projection.query_balance("ACC_123")
print(f" 🔎 Fast Query Result: ${final_balance}")
print("\n--- Auditing / Time Travel ---")
# The bank auditors want to know HOW we got that number.
history = store.get_stream("ACC_123")
print(" 📜 Audit Log:")
for i, e in enumerate(history):
print(f" Step {i+1}: {type(e).__name__} {e}")
print("\n [Disaster Recovery] Read DB crashed! Rebuilding from Event Store...")
rebuilt_balance = rebuild_state_from_events(history)
print(f" 🛠️ Rebuilt Balance: ${rebuilt_balance}")
print("\n✅ Event Sourcing & CQRS demonstrated successfully!")
Capstone: Patterns in the Wild
🧒 The 5-Year-Old Explanation
You've learned 26 different tools. A hammer, a screwdriver, a saw... But when you build a real house, you don't just use a hammer. You use all the tools together!
In the real world, Design Patterns don't live in isolation. They combine to form massive architectures.
🏗️ How Frameworks Combine Patterns
If you want to master Software Architecture, you must learn to see how these patterns intertwine. Let's look at a few examples:
1. Django (Python Web Framework)
Django is a massive combination of patterns working in harmony:
- Template Method: Django's Class-Based Views (
View,ListView,DetailView) use the Template Method to define the skeleton of an HTTP request, letting you overrideget(),post(), orgetcontextdata(). - Observer (PubSub): Django Signals (
postsave,predelete) allow completely decoupled components to react when database models change. - Active Record: (A variation of Repository/Data Mapper).
User.objects.get(id=1)binds the data and the database access into a single Model class. - Middleware / Chain of Responsibility: Every HTTP Request passes through a chain of Middleware (Authentication, CSRF, Sessions) before hitting your view.
2. SQLAlchemy (Python ORM)
- Unit of Work: The
Sessionobject. It tracks all changes to your objects and commits them atomically. - Identity Map: (Related to Flyweight/Singleton). If you query
User(id=1)twice in the same session, SQLAlchemy returns the exact same object in memory. - Iterator:
ResultProxybatches database results instead of loading millions of rows into RAM.
3. FastAPI (Python API Framework)
- Dependency Injection: FastAPI's core routing engine is a massive DI Container. You declare
db: Session = Depends(get_db), and the framework builds and injects it for you. - Decorator: You register routes using
@app.get("/"), adding routing behavior to your raw functions.
⚠️ The Danger Zone: Patternitis
The biggest mistake junior and mid-level developers make is Patternitis (or "Golden Hammer Syndrome").
"When you have a shiny new hammer, everything looks like a nail."
How to avoid Patternitis:
- YAGNI (You Aren't Gonna Need It): Do not implement a pattern because you might need it next year. Implement it when the pain of not having it becomes unbearable today.
- KISS (Keep It Simple, Stupid): A simple function is almost always better than a 5-class Strategy pattern. Only upgrade to the Strategy pattern when the simple function becomes a 200-line
if/elifnightmare. - Listen to the Language: Python is not Java. Python has first-class functions, decorators, and metaclasses. Often, a GoF pattern that takes 4 classes in Java takes 1 line of Python. Don't write Java in Python.
🎓 Conclusion
Design Patterns are not rules. They are a Shared Vocabulary.
When you say, "I implemented a Factory that returns a Strategy," another engineer immediately understands the entire architecture of your code without reading a single line.
Use them wisely, use them sparingly, and happy coding! 🚀