← Coding Interview Prep ← DS & Algorithms ← Advanced DSA

Python Design Patterns

Gang of Four patterns implemented the Pythonic way — Creational, Structural, Behavioral, plus SOLID principles.

5
Creational
4
Structural
5
Behavioral
5
SOLID

Creational Patterns

How objects are created — Singleton, Factory, Builder, Prototype, Abstract Factory

Singleton
CreationalMost Asked
Ensure only ONE instance of a class exists. Used for: DB connections, config, logging, caches.
click to expand
Client A ──→ getInstance() ──→ ┌──────────┐ │ Singleton │ ← same object Client B ──→ getInstance() ──→ │ instance │ └──────────┘
Method 1: Using __new__ (Classic)
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # True — same object!
Method 2: Using Decorator (Pythonic)
def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self):
        self.connection = "connected"

db1 = Database()
db2 = Database()
print(db1 is db2)  # True
Method 3: Module-Level (Most Pythonic)
# config.py — Python modules are singletons by default!
class _Config:
    def __init__(self):
        self.debug = False
        self.db_url = "postgres://..."

config = _Config()  # module-level instance

# usage: from config import config
Thread-Safe Singleton
import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:  # double-check
                    cls._instance = super().__new__(cls)
        return cls._instance
When to use: Database connections, configuration, logging, thread pools, caches. Avoid when: Testing (hard to mock), tight coupling.
Interview tip: Know at least 2 methods. Mention thread safety if asked. The module-level approach is most Pythonic.
Factory Method & Abstract Factory
CreationalMost Asked
Create objects without specifying the exact class. Let subclasses or a function decide.
click to expand
Factory Method: create("pdf") ──→ PDFExporter() create("csv") ──→ CSVExporter() create("json") ──→ JSONExporter() Client doesn't know or care about concrete classes!
Simple Factory (Function)
from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message):
        pass

class EmailNotification(Notification):
    def send(self, message):
        return f"Email: {message}"

class SMSNotification(Notification):
    def send(self, message):
        return f"SMS: {message}"

class SlackNotification(Notification):
    def send(self, message):
        return f"Slack: {message}"

# Factory function
def create_notification(channel):
    factories = {
        "email": EmailNotification,
        "sms": SMSNotification,
        "slack": SlackNotification,
    }
    if channel not in factories:
        raise ValueError(f"Unknown channel: {channel}")
    return factories[channel]()

# Usage — client doesn't import concrete classes
notifier = create_notification("slack")
notifier.send("Hello!")  # "Slack: Hello!"
Abstract Factory (Family of Objects)
class UIFactory(ABC):
    @abstractmethod
    def create_button(self): pass
    @abstractmethod
    def create_input(self): pass

class DarkThemeFactory(UIFactory):
    def create_button(self): return DarkButton()
    def create_input(self): return DarkInput()

class LightThemeFactory(UIFactory):
    def create_button(self): return LightButton()
    def create_input(self): return LightInput()

# Client works with any factory
def build_ui(factory: UIFactory):
    btn = factory.create_button()
    inp = factory.create_input()
    return btn, inp
Factory Method: One product, multiple variants. Abstract Factory: Family of related products. In Python, dict-based factory is most common.
Builder
CreationalFluent API
Construct complex objects step by step. Avoids telescoping constructors.
click to expand
class QueryBuilder:
    def __init__(self):
        self._table = None
        self._conditions = []
        self._columns = ["*"]
        self._order = None
        self._limit = None

    def table(self, name):
        self._table = name
        return self          # return self for chaining!

    def select(self, *columns):
        self._columns = list(columns)
        return self

    def where(self, condition):
        self._conditions.append(condition)
        return self

    def order_by(self, column):
        self._order = column
        return self

    def limit(self, n):
        self._limit = n
        return self

    def build(self):
        q = f"SELECT {', '.join(self._columns)} FROM {self._table}"
        if self._conditions:
            q += f" WHERE {' AND '.join(self._conditions)}"
        if self._order:
            q += f" ORDER BY {self._order}"
        if self._limit:
            q += f" LIMIT {self._limit}"
        return q

# Fluent API usage
query = (QueryBuilder()
    .table("users")
    .select("name", "email")
    .where("age > 18")
    .where("active = true")
    .order_by("name")
    .limit(10)
    .build())
# SELECT name, email FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10
Pythonic Alternative: dataclass + optional fields
from dataclasses import dataclass, field

@dataclass
class ServerConfig:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False
    workers: int = 4
    middleware: list = field(default_factory=list)

config = ServerConfig(port=3000, debug=True)
Key: Return self from each method for chaining. In Python, dataclass with defaults often replaces Builder for simple cases.
Prototype
CreationalPythonic
Create new objects by cloning existing ones. Python has built-in support via copy.
click to expand
import copy

class Document:
    def __init__(self, title, content, styles):
        self.title = title
        self.content = content
        self.styles = styles   # mutable (dict/list)

    def clone(self):
        return copy.deepcopy(self)

# Create template
template = Document("Report", "", {"font": "Arial", "size": 12})

# Clone and customize
doc1 = template.clone()
doc1.title = "Q1 Report"
doc1.content = "Q1 results..."

doc2 = template.clone()
doc2.title = "Q2 Report"
Shallow vs Deep: copy.copy() = shallow (nested objects shared). copy.deepcopy() = deep (fully independent). Use deep for mutable nested objects.
Object Pool
CreationalResource Management
Reuse expensive objects (DB connections, threads) instead of creating/destroying repeatedly.
click to expand
from queue import Queue

class ConnectionPool:
    def __init__(self, max_size=5):
        self._pool = Queue(maxsize=max_size)
        for _ in range(max_size):
            self._pool.put(self._create_connection())

    def _create_connection(self):
        return {"id": id(object()), "status": "open"}

    def acquire(self):
        return self._pool.get()     # blocks if empty

    def release(self, conn):
        self._pool.put(conn)

    # Context manager for automatic release
    def connection(self):
        return PooledConnection(self)

class PooledConnection:
    def __init__(self, pool):
        self.pool = pool
    def __enter__(self):
        self.conn = self.pool.acquire()
        return self.conn
    def __exit__(self, *args):
        self.pool.release(self.conn)

# Usage
pool = ConnectionPool(max_size=3)
with pool.connection() as conn:
    print(conn)  # auto-released when done
Key: Use Queue for thread safety. Context manager (with) ensures connections are always returned.

Structural Patterns

How objects are composed — Adapter, Decorator, Facade, Proxy

Adapter
StructuralCommon
Convert interface of one class to another. Makes incompatible classes work together.
click to expand
Old API: xml_data = legacy.get_xml() New API: json_data = service.get_json() Adapter wraps old API to look like new API: Client ──→ Adapter ──→ LegacySystem │ converts XML → JSON
# Legacy system (can't modify)
class LegacyPayment:
    def process_payment_usd(self, amount_cents):
        return f"Charged ${amount_cents / 100:.2f}"

# New interface your app expects
class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, amount_dollars: float):
        pass

# Adapter bridges old → new
class LegacyPaymentAdapter(PaymentProcessor):
    def __init__(self, legacy):
        self._legacy = legacy

    def pay(self, amount_dollars):
        cents = int(amount_dollars * 100)
        return self._legacy.process_payment_usd(cents)

# Usage — client only knows PaymentProcessor
processor = LegacyPaymentAdapter(LegacyPayment())
processor.pay(29.99)  # "Charged $29.99"
Key: Adapter wraps an existing object. Does NOT modify the adaptee. Common in integrating third-party libraries or legacy code.
Decorator
StructuralVery Pythonic
Add behavior to objects dynamically without modifying them. Python has built-in decorator syntax!
click to expand
Python Function Decorator (Most Common)
import time, functools

# Timer decorator
def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time()-start:.3f}s")
        return result
    return wrapper

# Retry decorator
def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    print(f"Retry {attempt+1}: {e}")
        return wrapper
    return decorator

# Cache decorator (built-in!)
from functools import lru_cache

@timer
@retry(max_attempts=3)
@lru_cache(maxsize=128)
def fetch_data(url):
    # stacks: timer → retry → cache → actual function
    return requests.get(url).json()
Class-Based Decorator (GoF Style)
class Coffee:
    def cost(self): return 5
    def description(self): return "Coffee"

class MilkDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self): return self._coffee.cost() + 2
    def description(self): return self._coffee.description() + ", Milk"

class SugarDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    def cost(self): return self._coffee.cost() + 1
    def description(self): return self._coffee.description() + ", Sugar"

order = SugarDecorator(MilkDecorator(Coffee()))
print(order.description())  # "Coffee, Milk, Sugar"
print(order.cost())         # 8
Key: Always use @functools.wraps(func) to preserve the original function's name and docstring. Decorators stack bottom-up.
Facade
StructuralSimplifier
Simple interface to a complex subsystem. Hide the complexity behind one clean API.
click to expand
# Complex subsystems
class Inventory:
    def check_stock(self, item): return True
    def reduce_stock(self, item): pass

class Payment:
    def charge(self, card, amount): return True

class Shipping:
    def create_label(self, address): return "SHIP-123"

class EmailService:
    def send_confirmation(self, email): pass

# Facade — one simple method
class OrderFacade:
    def __init__(self):
        self._inventory = Inventory()
        self._payment = Payment()
        self._shipping = Shipping()
        self._email = EmailService()

    def place_order(self, item, card, address, email):
        if not self._inventory.check_stock(item):
            raise Exception("Out of stock")
        self._payment.charge(card, item.price)
        self._inventory.reduce_stock(item)
        tracking = self._shipping.create_label(address)
        self._email.send_confirmation(email)
        return tracking

# Client: one call instead of 5
order = OrderFacade()
order.place_order(item, card, address, email)
Key: Facade doesn't add new functionality — it simplifies existing complexity. Common in APIs, SDKs, and service layers.
Proxy
StructuralAccess Control
Placeholder that controls access to another object. Types: lazy, protection, caching, logging.
click to expand
# Lazy Loading Proxy
class HeavyImage:
    def __init__(self, path):
        self.path = path
        self._load()         # expensive!
    def _load(self):
        print(f"Loading {self.path}...")
    def display(self):
        print(f"Displaying {self.path}")

class ImageProxy:
    def __init__(self, path):
        self.path = path
        self._image = None   # lazy — don't load yet

    def display(self):
        if self._image is None:
            self._image = HeavyImage(self.path)  # load on first use
        self._image.display()

# Caching Proxy
class CachingProxy:
    def __init__(self, service):
        self._service = service
        self._cache = {}

    def get_data(self, key):
        if key not in self._cache:
            self._cache[key] = self._service.get_data(key)
        return self._cache[key]
Proxy vs Decorator: Proxy controls ACCESS to the object. Decorator adds BEHAVIOR. Proxy often creates/manages the real object internally.

Behavioral Patterns

How objects communicate — Observer, Strategy, Command, Iterator, State

Observer (Pub/Sub)
BehavioralMost Asked
When one object changes, all dependents are notified automatically. Event-driven architecture.
click to expand
Subject (Publisher) Observers (Subscribers) ┌──────────────┐ ┌───────────┐ │ EventSystem │──notify──→ │ EmailAlert│ │ │──notify──→ │ SlackAlert│ │ subscribers │──notify──→ │ Dashboard │ └──────────────┘ └───────────┘
class EventSystem:
    def __init__(self):
        self._subscribers = {}  # event_name → [callbacks]

    def subscribe(self, event, callback):
        if event not in self._subscribers:
            self._subscribers[event] = []
        self._subscribers[event].append(callback)

    def unsubscribe(self, event, callback):
        self._subscribers[event].remove(callback)

    def emit(self, event, data=None):
        for callback in self._subscribers.get(event, []):
            callback(data)

# Usage
events = EventSystem()

def send_email(user): print(f"Email to {user}")
def log_signup(user): print(f"Log: {user} signed up")
def send_slack(user): print(f"Slack: new user {user}")

events.subscribe("user_signup", send_email)
events.subscribe("user_signup", log_signup)
events.subscribe("user_signup", send_slack)

events.emit("user_signup", "chalamaiah")
# All 3 functions called automatically!
Key: Decouples publisher from subscribers. Publisher doesn't need to know WHO is listening. Used in: event systems, MVC, reactive programming, message queues.
Strategy
BehavioralMost Asked
Define a family of algorithms, make them interchangeable. Client chooses at runtime.
click to expand
Class-Based (Traditional)
from abc import ABC, abstractmethod

class PricingStrategy(ABC):
    @abstractmethod
    def calculate(self, price): pass

class RegularPricing(PricingStrategy):
    def calculate(self, price): return price

class PremiumPricing(PricingStrategy):
    def calculate(self, price): return price * 0.8  # 20% off

class BlackFridayPricing(PricingStrategy):
    def calculate(self, price): return price * 0.5  # 50% off

class ShoppingCart:
    def __init__(self, strategy: PricingStrategy):
        self._strategy = strategy

    def checkout(self, price):
        return self._strategy.calculate(price)

cart = ShoppingCart(BlackFridayPricing())
cart.checkout(100)  # 50.0
Pythonic (Functions as Strategies)
# In Python, functions ARE objects — no need for classes!
def sort_by_name(users): return sorted(users, key=lambda u: u["name"])
def sort_by_age(users):  return sorted(users, key=lambda u: u["age"])
def sort_by_score(users): return sorted(users, key=lambda u: -u["score"])

strategies = {
    "name": sort_by_name,
    "age": sort_by_age,
    "score": sort_by_score,
}

# Pick strategy at runtime
result = strategies["age"](users)
Key: In Python, first-class functions often replace Strategy classes. Dict of functions = Pythonic strategy pattern. Use classes when strategies have state.
Command
BehavioralUndo/Redo
Encapsulate a request as an object. Enables undo/redo, queuing, and logging of operations.
click to expand
from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self): pass
    @abstractmethod
    def undo(self): pass

class AddTextCommand(Command):
    def __init__(self, doc, text):
        self.doc = doc
        self.text = text

    def execute(self):
        self.doc.content += self.text

    def undo(self):
        self.doc.content = self.doc.content[:-len(self.text)]

class Editor:
    def __init__(self):
        self.content = ""
        self._history = []

    def execute(self, command):
        command.execute()
        self._history.append(command)

    def undo(self):
        if self._history:
            cmd = self._history.pop()
            cmd.undo()

# Usage
editor = Editor()
editor.execute(AddTextCommand(editor, "Hello "))
editor.execute(AddTextCommand(editor, "World"))
print(editor.content)   # "Hello World"
editor.undo()
print(editor.content)   # "Hello "
Key: Each command stores enough state to undo itself. History stack enables unlimited undo. Used in: text editors, transactions, task queues.
Iterator & Generator
BehavioralVery Pythonic
Traverse a collection without exposing internals. Python's yield makes this trivial.
click to expand
Iterator Protocol
class CountDown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        val = self.current
        self.current -= 1
        return val

for n in CountDown(5):
    print(n)  # 5,4,3,2,1
Generator (Pythonic)
# yield makes it trivial!
def countdown(n):
    while n > 0:
        yield n
        n -= 1

for n in countdown(5):
    print(n)

# Infinite generator
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Lazy — only computes on demand
fib = fibonacci()
[next(fib) for _ in range(10)]
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Key: Implement __iter__ + __next__ for iterator protocol. Or just use yield (generator). Generators are lazy — memory efficient for large datasets.
State
BehavioralState Machine
Object changes behavior when its internal state changes. Appears to change its class.
click to expand
Order States: [Pending] ──confirm──→ [Confirmed] ──ship──→ [Shipped] ──deliver──→ [Delivered] │ │ └──cancel──→ [Cancelled] ←──cancel──┘
from abc import ABC, abstractmethod

class OrderState(ABC):
    @abstractmethod
    def confirm(self, order): pass
    @abstractmethod
    def ship(self, order): pass
    @abstractmethod
    def cancel(self, order): pass

class PendingState(OrderState):
    def confirm(self, order):
        order.state = ConfirmedState()
        return "Order confirmed!"
    def ship(self, order):
        return "Can't ship — not confirmed yet"
    def cancel(self, order):
        order.state = CancelledState()
        return "Order cancelled"

class ConfirmedState(OrderState):
    def confirm(self, order): return "Already confirmed"
    def ship(self, order):
        order.state = ShippedState()
        return "Order shipped!"
    def cancel(self, order):
        order.state = CancelledState()
        return "Order cancelled"

class ShippedState(OrderState):
    def confirm(self, order): return "Already shipped"
    def ship(self, order): return "Already shipped"
    def cancel(self, order): return "Can't cancel — already shipped"

class CancelledState(OrderState):
    def confirm(self, order): return "Cancelled — can't confirm"
    def ship(self, order): return "Cancelled — can't ship"
    def cancel(self, order): return "Already cancelled"

class Order:
    def __init__(self):
        self.state = PendingState()
    def confirm(self):  return self.state.confirm(self)
    def ship(self):     return self.state.ship(self)
    def cancel(self):   return self.state.cancel(self)

order = Order()
print(order.ship())     # "Can't ship — not confirmed yet"
print(order.confirm())  # "Order confirmed!"
print(order.ship())     # "Order shipped!"
print(order.cancel())   # "Can't cancel — already shipped"
Key: Each state is a class. Transitions change order.state. Eliminates massive if/elif chains. Used in: order processing, game states, UI workflows.

Pythonic Patterns

Python-specific patterns using dunder methods, context managers, and metaclasses

Context Manager (with statement)
PythonicMust Know
Automatic resource cleanup — files, locks, DB connections. Guarantees cleanup even on exceptions.
click to expand
Class-Based
class Timer:
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        self.elapsed = time.time() - self.start
        print(f"Took {self.elapsed:.3f}s")

with Timer() as t:
    # do work...
    time.sleep(1)
Using contextmanager
from contextlib import contextmanager

@contextmanager
def managed_file(path):
    f = open(path, 'w')
    try:
        yield f       # give to caller
    finally:
        f.close()     # always cleanup

with managed_file("out.txt") as f:
    f.write("Hello")
Key: __enter__ sets up, __exit__ tears down. __exit__ runs even if exception occurs. Use @contextmanager + yield for quick version.
Dunder Methods (Magic Methods)
PythonicMust Know
Make your classes behave like built-in types — printable, comparable, iterable, callable.
click to expand
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = amount
        self.currency = currency

    def __repr__(self):          # developer-friendly string
        return f"Money({self.amount}, '{self.currency}')"

    def __str__(self):           # user-friendly string
        return f"${self.amount:.2f}"

    def __add__(self, other):   # m1 + m2
        return Money(self.amount + other.amount)

    def __eq__(self, other):    # m1 == m2
        return self.amount == other.amount

    def __lt__(self, other):    # m1 < m2 (enables sorting)
        return self.amount < other.amount

    def __len__(self):          # len(m)
        return int(self.amount)

    def __bool__(self):         # if m:
        return self.amount > 0

    def __call__(self, rate):   # m(1.2) — makes object callable
        return Money(self.amount * rate)
MethodTriggered byPurpose
__init__obj = Class()Constructor
__repr__repr(obj)Debug string
__str__str(obj), print(obj)Display string
__eq__, __lt__==, <Comparison
__add__, __mul__+, *Arithmetic
__len__len(obj)Length
__getitem__obj[key]Indexing
__iter__, __next__for x in objIteration
__call__obj()Make callable
__enter__, __exit__with objContext manager
Dataclasses & Property
PythonicModern Python
Reduce boilerplate for data-holding classes. @property for computed attributes.
click to expand
from dataclasses import dataclass, field

@dataclass
class User:
    name: str
    email: str
    age: int = 0
    tags: list = field(default_factory=list)

    # Auto-generates: __init__, __repr__, __eq__

@dataclass(frozen=True)  # immutable
class Point:
    x: float
    y: float

# @property — computed attribute with getter/setter
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius must be positive")
        self._radius = value

    @property
    def area(self):          # read-only computed
        return 3.14159 * self._radius ** 2

c = Circle(5)
print(c.area)      # 78.54 — accessed like attribute, computed like method
Key: @dataclass auto-generates __init__, __repr__, __eq__. Use frozen=True for immutable. field(default_factory=list) for mutable defaults.

SOLID Principles

Five principles of object-oriented design — the foundation of clean architecture

S — Single Responsibility Principle
SOLID
A class should have only ONE reason to change. One class = one job.
click to expand
Bad
class User:
    def save_to_db(self): ...
    def send_email(self): ...
    def generate_report(self): ...
# 3 reasons to change!
Good
class User: ...           # data only
class UserRepository: ...  # DB operations
class EmailService: ...    # email logic
class ReportGenerator: ... # reports
O — Open/Closed Principle
SOLID
Open for extension, closed for modification. Add new behavior without changing existing code.
click to expand
Bad — modify existing code
def calculate_area(shape):
    if shape.type == "circle":
        return 3.14 * shape.r ** 2
    elif shape.type == "rect":
        return shape.w * shape.h
    # Must modify to add triangle!
Good — extend via new class
class Shape(ABC):
    @abstractmethod
    def area(self): pass

class Circle(Shape):
    def area(self): return 3.14*self.r**2

class Triangle(Shape):  # NEW!
    def area(self): return ...
# No existing code changed
L — Liskov Substitution Principle
SOLID
Subtypes must be substitutable for their base types without breaking behavior.
click to expand
Bad — Square breaks Rectangle
class Rectangle:
    def set_width(self, w): self.w = w
    def set_height(self, h): self.h = h

class Square(Rectangle):
    def set_width(self, w):
        self.w = self.h = w  # surprise!
# Violates expectations of Rectangle
Good — separate abstractions
class Shape(ABC):
    @abstractmethod
    def area(self): pass

class Rectangle(Shape):
    def area(self): return self.w * self.h

class Square(Shape):
    def area(self): return self.side ** 2
# Both work wherever Shape is expected
I — Interface Segregation Principle
SOLID
Don't force classes to implement methods they don't use. Many small interfaces > one fat interface.
click to expand
Bad — fat interface
class Worker(ABC):
    @abstractmethod
    def work(self): pass
    @abstractmethod
    def eat(self): pass

class Robot(Worker):
    def work(self): ...
    def eat(self): pass  # forced!
Good — segregated
class Workable(ABC):
    @abstractmethod
    def work(self): pass

class Eatable(ABC):
    @abstractmethod
    def eat(self): pass

class Robot(Workable): # only work
    def work(self): ...
D — Dependency Inversion Principle
SOLID
Depend on abstractions, not concretions. High-level modules shouldn't depend on low-level modules.
click to expand
Bad — depends on concrete
class MySQLDatabase:
    def save(self, data): ...

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # locked in!
Good — depends on abstract
class Database(ABC):
    @abstractmethod
    def save(self, data): pass

class UserService:
    def __init__(self, db: Database):
        self.db = db  # inject any DB!
Key: Inject dependencies via constructor. Makes testing easy (pass mock DB). This is Dependency Injection — the most practical SOLID principle.

Pattern Cheat Sheet

Quick reference — which pattern to use when

ProblemPatternPython Shortcut
Need exactly one instanceSingletonModule-level variable
Create objects without knowing classFactoryDict of classes
Build complex objects step by stepBuilder@dataclass with defaults
Clone existing objectsPrototypecopy.deepcopy()
Make incompatible interfaces workAdapterWrapper class
Add behavior dynamicallyDecorator@decorator syntax
Simplify complex subsystemFacadeWrapper function/class
Control access to objectProxy@property, __getattr__
Notify on state changesObserverCallback lists, signals
Swap algorithms at runtimeStrategyDict of functions
Undo/redo operationsCommandCommand objects + history stack
Traverse collectionIteratoryield (generator)
Object behaves differently per stateStateState classes
Auto cleanup resourcesContext Managerwith + __enter__/__exit__