Week 17 - Pythonic Design Patterns¶
17.1 Conceptual Core¶
The GoF book describes patches around C++ and Java limitations: no first-class functions, no closures, no duck typing, mandatory class hierarchies. Many "patterns" in Python collapse to language features. Some still apply. Knowing which is which is senior-level taste.
17.2 The Catalog, Translated¶
| GoF Pattern | Pythonic Form |
|---|---|
| Strategy | A function passed as an argument. Or a Protocol. |
| Template Method | A function with hooks; ABC + abstractmethod only when you need enforcement. |
| Factory / Abstract Factory | A function. Or __init_subclass__ registry. Or functools.singledispatch. |
| Singleton | A module. import is the singleton. @lru_cache(maxsize=None) on a constructor for parameterized singletons. |
| Observer | signal/blinker, asyncio Queue, or just a list of callbacks. |
| Iterator | Built into the language (__iter__). |
| Decorator (GoF) | Decorators (Python). |
| Adapter | A function. Or Protocol + a thin wrapper class. |
| Visitor | functools.singledispatch for type dispatch; match statement for ADTs. |
| Command | A callable. functools.partial for binding. |
| Chain of Responsibility | Middleware. ASGI/WSGI middleware is exactly this. |
| State | Functions returning functions; or a match over an Enum. |
| Builder | Keyword args + dataclass. Rarely a builder class. |
| Flyweight | sys.intern for strings; __slots__ + class-level constants. |
| Proxy | __getattr__-based forwarding; or weakref.proxy. |
| Composite | A type that contains itself: tree: Node | list[Node]. |
| Memento | dataclasses.replace + immutability. |
| Mediator | An event bus or a domain service. |
The patterns that survive idiomatically: Strategy (via Protocol), Observer (via async queues), Chain (via middleware), Visitor (via singledispatch / match).
17.3 Architectural Patterns¶
- Hexagonal / Ports and Adapters. Domain at the core, adapters at the edge (HTTP, DB, message bus). Test the domain in isolation. Architecture Patterns with Python (Percival/Gregory) is the canonical Python treatment.
- Repository pattern: abstract persistence behind a
Protocol. Tests use an in-memory fake; production uses SQL. - Unit of Work: collect domain mutations, commit atomically. Pairs with SQLAlchemy session.
- CQRS-lite: separate read models from write models when their shapes diverge. Don't over-apply.
17.4 Lab - "Refactor a Junk Drawer"¶
- Take a 1k-LOC script of mixed responsibilities. Extract:
domain/,adapters/,service/,entrypoints/. WriteProtocols for the seams. - Add a fake repository for tests; the real one talks to SQLite. Run the same test suite against both.
- Document, in a
docs/architecture.md, why each module exists and what it depends on.
17.5 Idiomatic & Linter Drill¶
import-linterto enforce the dependency direction (entrypoints → service → domain ← adapters).