ML Algorithms
Python
29 / 29

Design Patterns

Classic patterns, written the Pythonic way — often shorter than the Java version.

Patterns ≠ rules
Patterns are vocabulary for solutions that keep appearing. In Python, many of them collapse into a function or a Protocol — don't import boilerplate you don't need.

1. Strategy

Swap the algorithm without changing the caller. In Python, functions are strategies.

def quick_sort(xs):  ...
def merge_sort(xs):  ...

def sort_with(xs, strategy=quick_sort):
    return strategy(xs)

sort_with(data, strategy=merge_sort)

2. Factory

Centralize object creation so callers don't depend on concrete classes.

def make_model(name: str):
    return {
        "lr":   LogisticRegression(),
        "rf":   RandomForestClassifier(),
        "xgb":  XGBClassifier(),
    }[name]

model = make_model(config["model"])

3. Singleton

One instance app-wide. In Python, a module is already a singleton — use that first.

# settings.py
class _Settings:
    db_url = os.environ["DB_URL"]
    debug  = False

settings = _Settings()        # imported everywhere; one instance

4. Observer (Pub/Sub)

class EventBus:
    def __init__(self):
        self._subs: dict[str, list[Callable]] = defaultdict(list)
    def on(self, event, handler):  self._subs[event].append(handler)
    def emit(self, event, payload):
        for h in self._subs[event]: h(payload)

bus = EventBus()
bus.on("order.placed", send_confirmation_email)
bus.on("order.placed", update_inventory)
bus.emit("order.placed", order)

5. Adapter

Wrap an incompatible interface so it matches the one your code expects.

class StripeChargesAPI:
    def create_charge(self, cents, card): ...

class PaymentGateway(Protocol):
    def pay(self, amount: Money, method: PaymentMethod) -> Receipt: ...

class StripeAdapter:
    def __init__(self, api: StripeChargesAPI): self.api = api
    def pay(self, amount, method):
        raw = self.api.create_charge(amount.cents, method.card)
        return Receipt.from_stripe(raw)

6. Decorator

Wrap behavior around a function — caching, retries, auth, metrics.

def retry(times=3):
    def deco(fn):
        @functools.wraps(fn)
        def wrapper(*a, **kw):
            for i in range(times):
                try:    return fn(*a, **kw)
                except TransientError:
                    if i == times - 1: raise
                    time.sleep(2 ** i)
        return wrapper
    return deco

@retry(times=5)
def fetch_user(id): ...

7. Repository

Hide persistence behind a Pythonic collection interface — testable, swappable.

class UserRepo(Protocol):
    def get(self, id: int) -> User | None: ...
    def add(self, user: User) -> None: ...

class SqlUserRepo:       def get(self, id): ...
class InMemoryUserRepo:  def get(self, id): ...   # for tests

When NOT to use a pattern

  • You have one implementation and no plan for a second — YAGNI.
  • The pattern adds more code than the problem it solves.
  • You're copying Java/C# blueprints verbatim — Python usually has a one-liner.
The Pythonic rule
Reach for a Protocol + a function before reaching for a class hierarchy. Patterns should appear because the code needed them, not because you wanted to use one.