Python Design Patterns
Pythonic adaptations of GoF patterns, SOLID principles, and anti-patterns to avoid.
"Modern Python simply avoids the problems that the old design patterns were meant to solve."
— Brandon Rhodes, PyCon 2025
Prerequisites: from __future__ import annotations, collections.abc for Callable/Iterator, structlog for logging.
Pattern Decision Table
| Need |
Pattern |
Python Idiom |
| Multiple creation strategies |
Factory |
Function or @classmethod |
| Complex object, many options |
Builder |
dataclass + replace() |
| Interchangeable algorithms |
Strategy |
Pass Callable |
| Event notification |
Observer |
Callback list |
| Lazy sequences |
Iterator |
Generator (yield) |
| Tree structures |
Composite |
Recursive dataclass |
| Memory optimization |
Flyweight |
__slots__, lru_cache |
| Interface conversion |
Adapter |
Wrapper class |
| Cross-cutting concerns |
Decorator |
@decorator |
| State machines |
State |
Dict mapping or match |
| Shared configuration |
Global Object |
Module-level instance |
| Callbacks with state |
Prebound Method |
obj.method, partial |
| None is valid value |
Sentinel |
_MISSING = object() |
SOLID Quick Reference
| Principle |
Violation Sign |
Fix |
| Single Responsibility |
"Manager" class |
Split into focused classes |
| Open-Closed |
if/elif on types |
Dict of handlers + Protocol |
| Liskov Substitution |
NotImplementedError |
Composition over inheritance |
| Interface Segregation |
Unused interface methods |
Small Protocol classes |
| Dependency Inversion |
self.db = PostgresDB() |
Constructor injection |
Anti-Patterns
| Anti-Pattern |
Why Bad |
Pythonic Alternative |
| Singleton class |
Modules ARE singletons |
Module-level instance |
| Deep inheritance |
Coupling, diamond problem |
Composition + Protocol |
| Java getters/setters |
Boilerplate |
Direct attrs, @property |
| God Object |
Untestable, SRP violation |
Focused services |
| Method without self |
Unnecessary class |
Module-level function |
| Premature patterns |
Over-engineering |
YAGNI - add when needed |
Key Patterns
Factory & Strategy
# Factory: function returning instance
def create_connection(db_type: str) -> Connection:
return {"postgres": PostgresConn, "sqlite": SQLiteConn}[db_type]()
# Strategy: pass functions, not strategy objects
def calculate_total(items: list[Item], pricing: Callable[[float], float]) -> float:
return sum(pricing(i.price) for i in items)
Observer & Decorator
# Observer: callback list
@dataclass
class EventEmitter:
_listeners: dict[str, list[Callable]] = field(default_factory=dict)
def on(self, event: str, fn: Callable) -> None:
self._listeners.setdefault(event, []).append(fn)
def emit(self, event: str, *args) -> None:
for fn in self._listeners.get(event, []): fn(*args)
# Decorator: @wraps for cross-cutting concerns
def timing(func):
@wraps(func)
def wrapper(*a, **kw):
start = perf_counter()
result = func(*a, **kw)
log.info("timing", elapsed=perf_counter() - start)
return result
return wrapper
Global Object & Sentinel
# Global Object: module-level instance (not singleton class)
_config: Config | None = None
def get_config() -> Config:
global _config
if _config is None: _config = Config.from_env()
return _config
# Sentinel: distinguish None from "not provided"
_MISSING = object()
def get(key: str, default: object = _MISSING) -> object:
if (v := cache.get(key)) is not None: return v
if default is _MISSING: raise KeyError(key)
return default
Reference Files
| File |
Content |
references/patterns.md |
All GoF patterns with examples |
references/solid-anti.md |
SOLID principles + anti-patterns |
references/python-idioms.md |
Global Object, Prebound Method, Sentinel |
Related
1---2name: python-design-patterns3description: This skill should be used when the user asks about "design patterns", "SOLID principles", "factory pattern", "strategy pattern", "observer pattern", "composition vs inheritance", "Pythonic design", "singleton alternatives", "anti-patterns", "dependency injection", or needs guidance on applying Gang of Four patterns idiomatically in Python.4---5
6# Python Design Patterns
7
8Pythonic adaptations of GoF patterns, SOLID principles, and anti-patterns to avoid.
9
10> "Modern Python simply avoids the problems that the old design patterns were meant to solve."
11> — Brandon Rhodes, PyCon 2025
12
13**Prerequisites:** `from __future__ import annotations`, `collections.abc` for Callable/Iterator, `structlog` for logging.
14
15## Pattern Decision Table
16
17| Need | Pattern | Python Idiom |
18|------|---------|--------------|
19| Multiple creation strategies | Factory | Function or `@classmethod` |
20| Complex object, many options | Builder | `dataclass` + `replace()` |
21| Interchangeable algorithms | Strategy | Pass `Callable` |
22| Event notification | Observer | Callback list |
23| Lazy sequences | Iterator | Generator (`yield`) |
24| Tree structures | Composite | Recursive dataclass |
25| Memory optimization | Flyweight | `__slots__`, `lru_cache` |
26| Interface conversion | Adapter | Wrapper class |
27| Cross-cutting concerns | Decorator | `@decorator` |
28| State machines | State | Dict mapping or `match` |
29| Shared configuration | Global Object | Module-level instance |
30| Callbacks with state | Prebound Method | `obj.method`, `partial` |
31| None is valid value | Sentinel | `_MISSING = object()` |
32
33## SOLID Quick Reference
34
35| Principle | Violation Sign | Fix |
36|-----------|----------------|-----|
37| **S**ingle Responsibility | "Manager" class | Split into focused classes |
38| **O**pen-Closed | `if/elif` on types | Dict of handlers + Protocol |
39| **L**iskov Substitution | `NotImplementedError` | Composition over inheritance |
40| **I**nterface Segregation | Unused interface methods | Small `Protocol` classes |
41| **D**ependency Inversion | `self.db = PostgresDB()` | Constructor injection |
42
43## Anti-Patterns
44
45| Anti-Pattern | Why Bad | Pythonic Alternative |
46|--------------|---------|---------------------|
47| Singleton class | Modules ARE singletons | Module-level instance |
48| Deep inheritance | Coupling, diamond problem | Composition + Protocol |
49| Java getters/setters | Boilerplate | Direct attrs, `@property` |
50| God Object | Untestable, SRP violation | Focused services |
51| Method without self | Unnecessary class | Module-level function |
52| Premature patterns | Over-engineering | YAGNI - add when needed |
53
54## Key Patterns
55
56### Factory & Strategy
57
58```python
59# Factory: function returning instance
60def create_connection(db_type: str) -> Connection:
61 return {"postgres": PostgresConn, "sqlite": SQLiteConn}[db_type]()
62
63# Strategy: pass functions, not strategy objects
64def calculate_total(items: list[Item], pricing: Callable[[float], float]) -> float:
65 return sum(pricing(i.price) for i in items)
66```
67
68### Observer & Decorator
69
70```python
71# Observer: callback list
72@dataclass
73class EventEmitter:
74 _listeners: dict[str, list[Callable]] = field(default_factory=dict)
75 def on(self, event: str, fn: Callable) -> None:
76 self._listeners.setdefault(event, []).append(fn)
77 def emit(self, event: str, *args) -> None:
78 for fn in self._listeners.get(event, []): fn(*args)
79
80# Decorator: @wraps for cross-cutting concerns
81def timing(func):
82 @wraps(func)
83 def wrapper(*a, **kw):
84 start = perf_counter()
85 result = func(*a, **kw)
86 log.info("timing", elapsed=perf_counter() - start)
87 return result
88 return wrapper
89```
90
91### Global Object & Sentinel
92
93```python
94# Global Object: module-level instance (not singleton class)
95_config: Config | None = None
96def get_config() -> Config:
97 global _config
98 if _config is None: _config = Config.from_env()
99 return _config
100
101# Sentinel: distinguish None from "not provided"
102_MISSING = object()
103def get(key: str, default: object = _MISSING) -> object:
104 if (v := cache.get(key)) is not None: return v
105 if default is _MISSING: raise KeyError(key)
106 return default
107```
108
109## Reference Files
110
111| File | Content |
112|------|---------|
113| `references/patterns.md` | All GoF patterns with examples |
114| `references/solid-anti.md` | SOLID principles + anti-patterns |
115| `references/python-idioms.md` | Global Object, Prebound Method, Sentinel |
116
117## Related
118
119- [ADR-014](../../docs/adr/014-design-patterns-skill.md) - Design decisions
120- [ADR-013](../../docs/adr/013-immutability-safety-patterns.md) - Immutability patterns