Codeskill

Learn to code, step by step

Design patterns without Java cosplay

Design patterns in Python matters once your pages get past the basics. The useful ones, expressed the Python way. Patterns are shared names for recurring problems. They are not a checklist to implement in every project, and they are definitely not an excuse to build a UML diagram before you write ten lines of code.

Python already is the pattern

Many Gang-of-Four patterns collapse in Python because the language gives you first-class functions, duck typing, and simple modules. A “strategy” is often a callable passed as an argument. A “factory” is a function that returns the right class. Do not reach for classes when a function will do.

def apply_discount(base: float, rule) -> float:
    return rule(base)

def ten_percent_off(amount: float) -> float:
    return amount * 0.9

def flat_two_off(amount: float) -> float:
    return max(0, amount - 2)

print(apply_discount(50, ten_percent_off))

Composition over inheritance

Deep inheritance trees are brittle. Prefer small objects that wrap or delegate to collaborators.

class ConsoleNotifier:
    def send(self, message: str) -> None:
        print(message)

class OrderService:
    def __init__(self, notifier: ConsoleNotifier) -> None:
        self.notifier = notifier

    def place_order(self, item: str) -> None:
        # business logic here
        self.notifier.send(f"Order placed: {item}")

Dependency injection does not need a framework. Pass collaborators into __init__ or factory functions. Tests swap in fakes without subclassing half your app.

Registry and plugin style

A registry maps names to callables or classes. Flask blueprints, pytest plugins, and CLI subcommands all rhyme with this idea.

HANDLERS: dict[str, callable] = {}

def register(name: str):
    def decorator(func):
        HANDLERS[name] = func
        return func
    return decorator

@register("csv")
def export_csv(rows: list[dict]) -> str:
    return "csv data"

@register("json")
def export_json(rows: list[dict]) -> str:
    return '{"rows": ...}'

Context managers and the with pattern

Resource management is a pattern Python built into the language. Your own types can participate with __enter__ and __exit__, or the contextlib.contextmanager decorator for generator-style setup and teardown.

from contextlib import contextmanager

@contextmanager
def temp_env(key: str, value: str):
    import os
    old = os.environ.get(key)
    os.environ[key] = value
    try:
        yield
    finally:
        if old is None:
            os.environ.pop(key, None)
        else:
            os.environ[key] = old

Dataclasses for simple data carriers

When a object mostly holds data, a dataclass beats a hand-written __init__. Add methods when behaviour genuinely belongs there.

from dataclasses import dataclass

@dataclass(frozen=True)
class Address:
    line1: str
    city: str
    postcode: str

    def formatted(self) -> str:
        return f"{self.line1}, {self.city} {self.postcode}"

Patterns to use sparingly

  • Singleton – usually a module-level object or explicit app factory is clearer
  • Abstract factory hierarchies – only when you truly swap whole families of implementations
  • Visitor – rare in Python; often a dict dispatch or match statement reads better

Name patterns when they help your team communicate. Skip them when a plain function and a good module layout tell the story on their own.

PreviousPackaging and publishing mindset