
Implementation Design Patterns Python
- 70 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
implementation-design-patterns-python is a Claude Code skill for python. It helps solo builders move faster with AI-assisted coding.
Key points
- implementation-design-patterns-python
- Python
- AI-coding skill
Implementation Design Patterns Python by the numbers
- 70 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #122 of 290 Python skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill implementation-design-patterns-pythonAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 70 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I helps with python tasks during ai-assisted development?
Helps with python tasks during AI-assisted development.
Who is it for?
Best when you're working on python and need structured help with implementation-design-patterns-python.
Skip if: Teams with no python needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with python tasks during ai-assisted development, or when implementation-design-patterns-python is a claude code skill for python. it helps solo builders move faster with ai-assisted coding.
What you get
Structured output aligned to implementation-design-patterns-python: implementation-design-patterns-python; Python; AI-coding skill.
Files
Python Design Patterns Best Practices (Refactoring Guru)
Implementation reference for the 22 Gang of Four design patterns in idiomatic modern Python (3.10+), distilled from refactoring.guru. Each of the 22 pattern files across 3 categories captures intent, problem, solution, applicability (when to use AND when NOT to), a runnable Python example with output, implementation steps, pros/cons, and relations to sibling patterns.
This is the Pythonic-first companion to the TypeScript design-patterns skill. Most GoF patterns shrink to a language feature in Python — a function, a generator, a dataclass, functools.singledispatch, a match statement. Every entry leads with that idiomatic form and keeps the class-based GoF structure only where identity, stored state, runtime registration, or polymorphic dispatch genuinely earn it.
The patterns are a vocabulary for structural decisions, not a prescription. Reach for one only when its applicability criteria match — every entry includes a When NOT to Use section to guard against over-engineering, which is the more common failure with this catalog in Python.
When to Apply
- A constructor has grown to 10+ parameters (telescoping-constructor smell) or subclasses exist only to bake in parameter combinations
- A method branches on
kind/type/mode/statusto pick an algorithm or behavior — amatchorif/elifladder that grows with each variant - Integrating an incompatible third-party API, library, or legacy class whose method names don't match your code
- Modeling a tree-shaped domain (file systems, ASTs, UI trees, org charts) where leaves and branches must be treated uniformly
- Adding cross-cutting behavior at runtime — logging, caching, access control, compression — without subclassing
- Selecting an algorithm or behavior variant at runtime from config, user input, or environment
- Implementing undo/redo, history snapshots, transactional rollback, or queueing/scheduling of operations
- Coordinating many objects whose direct mutual references have become tangled — a hub that brokers communication
- Notifying many subscribers when something changes — event systems, reactive data flows
- Reviewing code that smells like a pattern is implicit (a giant
if isinstance(...), parallel class hierarchies, copy-pasted algorithm skeletons) — make it explicit, or collapse it to a Python idiom
Rule Categories
| # | Category | Impact | Patterns | When to reach for this group |
|---|---|---|---|---|
| 1 | Creational | HIGH | 5 | Object construction is non-trivial, varies by configuration, or risks tight coupling to concrete classes |
| 2 | Structural | HIGH | 7 | Composing classes/objects into larger structures while keeping parts substitutable |
| 3 | Behavioral | HIGH | 10 | Distributing responsibility and defining how objects collaborate at runtime |
How to Use
1. Recognize the shape. Read the Quick Reference below and identify which pattern's intent matches your problem. Most pattern-shaped problems sound like one of the listed phrases. 2. Read the pattern reference. Open references/{category}-{pattern}.md. Confirm intent, then read Applicability and When NOT to Use before adopting. 3. Lead with the idiom. Each "Correct" example shows the Pythonic form first. Adopt it unless you need the class-based structure shown in the Alternative block. 4. Adapt to your domain. The examples use small realistic domains (transports, route planners, document trees). Rename to your terms before merging. 5. Check the relations. Each entry ends with Related Patterns — siblings worth considering for the same problem.
Quick Reference
1. Creational Patterns (object instantiation)
- `creational-factory-method` — Resolve a concrete class through a registry/dispatch dict. "I want to pick a class by config/string key without an if/elif ladder." — HIGH
- `creational-abstract-factory` — Produce families of related objects that must match. "Switching one flag must swap a whole coordinated set (button + checkbox)." — MEDIUM-HIGH
- `creational-builder` — Construct complex objects step by step — in Python a keyword-only dataclass first. "My constructor has 10+ params, or I need staged assembly." — HIGH
- `creational-prototype` — Clone via
copy.deepcopy/dataclasses.replace. "I need another one just like this, with one value changed." — MEDIUM - `creational-singleton` — One shared instance via a module global or
functools.cache. "I need exactly one config/registry/pool, kept testable." — MEDIUM
2. Structural Patterns (composition)
- `structural-adapter` — Wrap a class so its interface matches what callers expect. "This library's method names don't match mine and I can't edit it." — HIGH
- `structural-bridge` — Split abstraction from implementation via composition +
Protocol. "Two orthogonal axes and the subclass count is exploding." — MEDIUM - `structural-composite` — Treat leaves and trees uniformly via a shared
Protocol+ recursion. "I have a tree and want one interface for items and groups." — HIGH - `structural-decorator` — Stack wrappers (or use
@decorator) to add behavior at runtime. "I want to layer logging + caching + compression in any order." — HIGH - `structural-facade` — Expose one function/module over a complex subsystem. "I just want `convert(file, fmt)`, not the codec/bitrate dance." — HIGH
- `structural-flyweight` — Share immutable state via a cached factory +
__slots__. "Millions of objects, only a few distinct payloads — out of RAM." — LOW-MEDIUM - `structural-proxy` — Stand in via
__getattr__/cached_propertyto control access. "I need lazy loading / auth / caching without touching the real object." — MEDIUM-HIGH
3. Behavioral Patterns (collaboration)
- `behavioral-chain-of-responsibility` — Run a request through an ordered list of handlers. "A pipeline of auth/validate/authorize checks I want to reorder." — MEDIUM-HIGH
- `behavioral-command` — Reify a request as a callable/closure with optional undo. "I need undo/redo, queueing, or one action shared across UI surfaces." — HIGH
- `behavioral-iterator` — Traverse via
__iter__/generators without exposing internals. "I want `for x in my_structure` to just work." — HIGH - `behavioral-mediator` — Route component interaction through one hub. "My widgets all reference each other and nothing is reusable." — MEDIUM
- `behavioral-memento` — Snapshot/restore state via a frozen dataclass. "I need undo/rollback without exposing private fields." — LOW-MEDIUM
- `behavioral-observer` — Notify subscriber callbacks on change (often a
propertysetter). "Many objects must react when one value changes — events, reactive UI." — CRITICAL - `behavioral-state` — Delegate to polymorphic state objects (or an enum + dispatch). "My class is a state machine with `if status ==` in every method." — MEDIUM-HIGH
- `behavioral-strategy` — Pass an algorithm as a
Callableand swap it at runtime. "Multiple algorithms (sort/route/pay) picked without conditionals." — HIGH - `behavioral-template-method` — Fix a skeleton in an ABC; subclasses override steps. "Several classes share an algorithm with a couple of varying steps." — MEDIUM
- `behavioral-visitor` — Add operations via
functools.singledispatch/match. "I need 5 operations across an AST without editing the node classes." — LOW-MEDIUM
How to Choose Between Similar Patterns
Several patterns share a shape but solve different problems. Read each pattern's Related Patterns section, then apply these distinctions:
- Adapter vs. Facade vs. Proxy vs. Decorator — all four wrap a target. Adapter changes the interface. Facade simplifies a subsystem. Proxy keeps the interface and controls access/lifecycle. Decorator keeps the interface and adds behavior recursively.
- Strategy vs. State — both delegate to a swapped object. Strategy variants are independent functions the caller picks. State objects know each other and trigger transitions on the context.
- Strategy vs. Template Method — both vary parts of an algorithm. Strategy uses composition — a
Callableswapped at runtime. Template Method uses inheritance — an ABC skeleton fixed at definition time. - Factory Method vs. Abstract Factory vs. Builder — Factory Method resolves one product (a registry/
@classmethod). Abstract Factory returns a family of matching products. Builder assembles one complex product (a keyword-only dataclass, or a fluent builder for staged construction). - Composite vs. Decorator — both wrap children recursively. Composite aggregates child results. Decorator adds one responsibility and passes through.
- Chain of Responsibility vs. Command vs. Mediator vs. Observer — all connect senders and receivers. CoR passes a request along a list of handlers (and may stop early). Command makes the request a first-class callable. Mediator centralizes many-to-many communication. Observer establishes one-publisher-to-many-subscribers notification.
- Visitor: `singledispatch` vs. `match` vs. methods — use
functools.singledispatchto add operations over a closed type set without editing the classes; usematchwhen you'd rather keep all cases in one exhaustive function; use plain methods when there's one operation and the type set is small.
References
1. Refactoring Guru — Design Patterns Catalog 2. Refactoring Guru — Python Examples 3. Refactoring Guru — Creational Patterns 4. Refactoring Guru — Structural Patterns 5. Refactoring Guru — Behavioral Patterns
Python Design Patterns
Version 0.1.0 Refactoring Guru May 2026
Note: This document is for agents and LLMs maintaining, generating, or refactoring Python Design Patterns code — the 22 Gang of Four patterns in idiomatic modern Python. Humans may also find it useful, but guidance here is optimized for AI-assisted workflows.
---
Abstract
Implementation guide for the 22 Gang of Four design patterns in idiomatic modern Python, distilled from refactoring.guru and adapted to current Python (3.10+) idioms. Each reference leads with the Pythonic form — first-class functions and Callable, dataclasses, typing.Protocol, functools.singledispatch, structural pattern matching (match), generators, copy/dataclasses.replace, __slots__, weakref, descriptors, decorators — and falls back to the class-based GoF structure only where identity, registration, or polymorphic dispatch genuinely require it. Each pattern covers intent, the problem it solves, the structural solution, applicability (when to use and when not to), a runnable Python example with output, implementation steps, pros/cons, and relations to sibling patterns. Patterns are grouped by purpose: 5 Creational (Factory Method, Abstract Factory, Builder, Prototype, Singleton), 7 Structural (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy), and 10 Behavioral (Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor). Use this skill when you recognize a pattern-shaped problem in Python — class explosion via inheritance, scattered conditionals branching on type, tight coupling between caller and concrete class, tree-shaped models, runtime algorithm selection, undo/redo, state-dependent behavior — and want a vetted, idiomatic recipe instead of porting a Java-shaped class hierarchy.
---
Table of Contents
1. Creational Patterns — HIGH
- 1.1 Use Abstract Factory to Produce Families of Related Objects — MEDIUM-HIGH (prevents mixing incompatible variants (a macOS checkbox with a Windows button) by guaranteeing every object from one factory belongs to the same family, eliminates parallel
if platform == ...conditionals at each widget-creation site) - 1.2 Use Builder to Construct Complex Objects Step by Step — HIGH (eliminates the telescoping-constructor smell (an
__init__with 10+ positional parameters and manyNonedefaults), prevents subclass explosion for parameter combinations, enables the same construction sequence to produce different representations) - 1.3 Use Factory Method to Decouple Object Creation from Concrete Classes — HIGH (eliminates direct
Truck()/Ship()constructor calls scattered through callers, isolates product instantiation so adding a new product type registers one class instead of editing every call site) - 1.4 Use Prototype to Clone Objects Without Coupling to Concrete Classes — MEDIUM (enables copying complex pre-configured objects through
copy.deepcopy/dataclasses.replacewithout a hand-written copy constructor, preserves nested mutable state automatically, removes the per-class copy code that silently rots when a field is added) - 1.5 Use Singleton to Guarantee a Single Shared Instance — MEDIUM (enforces exactly one instance of a shared resource (config, registry, connection pool, logger) via module-import caching or
functools.cache, prevents duplicate instantiation that diverges state, and keeps the object injectable for tests instead of a hidden global)
2. Structural Patterns — HIGH
- 2.1 Use Adapter to Make Incompatible Interfaces Cooperate — HIGH (enables reusing a class whose interface doesn't match what callers expect, eliminates ad-hoc conversion code scattered across call sites, isolates the third-party API translation in one wrapper)
- 2.2 Use Bridge to Split Abstraction from Implementation — MEDIUM (prevents exponential subclass explosion when a type varies along two independent dimensions (control type x device type), lets the abstraction and implementation evolve separately, enables swapping the implementation at runtime)
- 2.3 Use Composite to Treat Trees and Leaves Uniformly — HIGH (eliminates
isinstancebranching throughout traversal code, enables recursive operations across an object tree through one interface, lets clients work with arbitrarily nested structures without tracking depth) - 2.4 Use Decorator to Attach Behaviors at Runtime via Wrappers — HIGH (reduces N x M subclass explosion (every combination like CompressedEncryptedStream) to a few small wrappers stacked at runtime, eliminates duplicated wrapping code, enables adding or removing responsibilities dynamically)
- 2.5 Use Facade to Hide a Complex Subsystem Behind One Interface — HIGH (replaces sprawling client code that wires many subsystem objects with a single entry point, reduces coupling between application code and library internals, eliminates duplicated initialization sequences across callers)
- 2.6 Use Flyweight to Share Common State Across Many Objects — LOW-MEDIUM (cuts memory when spawning millions of similar objects (game particles, map tiles, glyphs) by sharing one immutable intrinsic-state object via a cache and passing variable extrinsic state per call, plus
__slots__to drop the per-instance__dict__) - 2.7 Use Proxy to Insert a Substitute Controlling Access to an Object — MEDIUM-HIGH (enables lazy loading, access control, caching, and logging without touching the real subject or duplicating that logic at each call site, preserves the original interface so callers stay unchanged)
3. Behavioral Patterns — HIGH
- 3.1 Use Chain of Responsibility to Pass Requests Through Handlers — MEDIUM-HIGH (replaces hardcoded validation/auth/parsing cascades with a composable list of handlers, enables reordering or inserting handlers without editing the others, eliminates deeply nested if/else that obscures pipeline intent)
- 3.2 Use Command to Turn Requests into Stand-Alone Objects — HIGH (enables undo/redo, queueing, and macro recording by reifying requests as callables or small command objects, decouples the invoker (button, shortcut) from the receiver (business logic), eliminates duplicated invocation logic across UI surfaces)
- 3.3 Use Iterator to Traverse Collections Without Exposing Their Internals — HIGH (hides a collection's representation behind the iterator protocol (list, tree, graph, stream all look the same to
for), enables multiple independent traversals, eliminates duplicated traversal code across the app) - 3.4 Use Mediator to Replace Many-to-Many Coupling with a Hub — MEDIUM (reduces N x N component dependencies to N x 1 by routing all interaction through one mediator, makes components reusable since they no longer reference each other directly)
- 3.5 Use Memento to Snapshot State Without Breaking Encapsulation — LOW-MEDIUM (captures restorable snapshots of an object's state through a narrow object so a caretaker (history, transaction log) can store them without seeing private fields, preserves encapsulation that exposing setters would break)
- 3.6 Use Observer to Broadcast State Changes to Many Subscribers — CRITICAL (enables one-to-many notification of state changes without the publisher knowing its subscribers — the foundation of event systems, reactive UI, pub/sub, and dataflow)
- 3.7 Use State to Alter Behavior When Internal State Changes — MEDIUM-HIGH (replaces sprawling
if self.status == ...blocks in every method with polymorphic state objects, eliminates duplicated state checks across methods, makes adding a state one new class instead of editing every method) - 3.8 Use Strategy to Make Algorithms Interchangeable at Runtime — HIGH (eliminates
if mode == "a": ... elif mode == "b": ...algorithm-selection conditionals scattered through business code, enables runtime swapping of algorithm variants, isolates each algorithm as an independently testable function) - 3.9 Use Template Method to Fix an Algorithm Skeleton and Let Subclasses Override Steps — MEDIUM (eliminates duplicated algorithm scaffolding across sibling classes by hoisting the shared sequence into a base method, lets subclasses override only the steps that vary, removes client conditionals that switch on subtype)
- 3.10 Use Visitor to Add Operations to Class Hierarchies Without Modifying Them — LOW-MEDIUM (enables adding operations (export, validate, render) across a closed object hierarchy by writing one
@singledispatchfunction instead of editing every node class, isolates each operation in one place rather than scattering it across the hierarchy)
---
References
1. https://refactoring.guru/design-patterns/catalog 2. https://refactoring.guru/design-patterns/python 3. https://refactoring.guru/design-patterns/creational-patterns 4. https://refactoring.guru/design-patterns/structural-patterns 5. https://refactoring.guru/design-patterns/behavioral-patterns
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and impact ordering |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
Gotchas
Append entries as they're discovered. Format:
### {Short title of the failure mode}
{What went wrong, how to recognize it, and how to avoid it.}
Added: {YYYY-MM-DD}---
Reach for the language feature before the class-based GoF template
The biggest mistake porting these patterns to Python is translating a Java-shaped class hierarchy verbatim. Most of the catalog collapses into a language feature:
- Strategy → pass a function /
Callabledirectly; no Strategy interface - Iterator → a generator (
yield) or__iter__;for,sum,inwork for free - Observer → a list of callbacks, or a
propertysetter; or a signals library - Command → a closure or
functools.partial(add anundoclosure for undo/redo) - Singleton → a module-level instance, or
functools.cacheon a factory function - Template Method → a higher-order function taking the varying steps as callables
- Visitor →
functools.singledispatch, or amatchstatement - Prototype →
copy.deepcopy/dataclasses.replace - Factory Method → a registry
dictkeyed by string, or a@classmethod - Builder → a keyword-only
@dataclasswith defaults - Proxy →
__getattr__delegation orfunctools.cached_property - Adapter → duck typing often means no adapter at all; otherwise a small wrapper function
Recommend the class-based form only when the user genuinely needs the extra structure: stored state, object identity, runtime registration, or polymorphic dispatch the language feature can't express. Over-engineering is the more common failure with this catalog than under-engineering. Added: 2026-05-21
Prefer typing.Protocol over forcing ABC inheritance
Several references type their interfaces as Protocol. Use a Protocol for structural ("duck") typing when implementers should NOT have to inherit a base class — adapters, strategies, plug-in products. Reach for abc.ABC only when you want enforced subclassing, shared concrete base logic (e.g., Template Method's skeleton), or isinstance checks at runtime. Forcing an ABC where a Protocol suffices couples every implementer to your base class and defeats the looseness Python gives you for free. Added: 2026-05-21
Don't use a mutable default or a class attribute for per-instance pattern state
Observer subscriber lists, Command history stacks, and Mediator component sets are mutable. Two traps: (1) a mutable default argument (def __init__(self, subs=[])) is shared across all instances and accumulates state between them; (2) a mutable class attribute (_subscribers: list = [] at class scope) is shared by every instance for the same reason. Initialize this state inside __init__ (self._subscribers = []) or use dataclasses.field(default_factory=list). The bug shows up as one subject mysteriously seeing another subject's subscribers. Added: 2026-05-21
functools.singledispatch keys on the first argument only — and methods need singledispatchmethod
The Pythonic Visitor uses @singledispatch, which dispatches on the runtime type of the first positional argument. It cannot dispatch on two arguments (no true double dispatch) and ignores type annotations at call time — only the concrete runtime type matters, so a subclass dispatches to its registered base unless separately registered. Inside a class, a plain @singledispatch would dispatch on self; use functools.singledispatchmethod and register on the second parameter instead. When dispatch needs more than one type or you prefer all cases in one place, use a match statement. Added: 2026-05-21
A functools.cache singleton must be reset between tests
The module-global / functools.cache Singleton is testable precisely because the accessor is a seam — but a cached instance persists across tests in the same process and leaks state. Call get_config.cache_clear() in test setup/teardown, or inject the dependency instead of importing the global. If you skip this, tests pass in isolation and fail when run together. Added: 2026-05-21
{
"version": "0.1.0",
"organization": "Refactoring Guru",
"technology": "Python Design Patterns",
"discipline": "distillation",
"type": "library-reference",
"date": "May 2026",
"abstract": "Implementation guide for the 22 Gang of Four design patterns in idiomatic modern Python, distilled from refactoring.guru and adapted to current Python (3.10+) idioms. Each reference leads with the Pythonic form — first-class functions and Callable, dataclasses, typing.Protocol, functools.singledispatch, structural pattern matching (match), generators, copy/dataclasses.replace, __slots__, weakref, descriptors, decorators — and falls back to the class-based GoF structure only where identity, registration, or polymorphic dispatch genuinely require it. Each pattern covers intent, the problem it solves, the structural solution, applicability (when to use and when not to), a runnable Python example with output, implementation steps, pros/cons, and relations to sibling patterns. Patterns are grouped by purpose: 5 Creational (Factory Method, Abstract Factory, Builder, Prototype, Singleton), 7 Structural (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy), and 10 Behavioral (Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor). Use this skill when you recognize a pattern-shaped problem in Python — class explosion via inheritance, scattered conditionals branching on type, tight coupling between caller and concrete class, tree-shaped models, runtime algorithm selection, undo/redo, state-dependent behavior — and want a vetted, idiomatic recipe instead of porting a Java-shaped class hierarchy.",
"references": [
"https://refactoring.guru/design-patterns/catalog",
"https://refactoring.guru/design-patterns/python",
"https://refactoring.guru/design-patterns/creational-patterns",
"https://refactoring.guru/design-patterns/structural-patterns",
"https://refactoring.guru/design-patterns/behavioral-patterns"
],
"category": "Design Patterns"
}
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group pattern references.
The 22 patterns are the original Gang of Four (GoF) catalog grouped by purpose: Creational (object instantiation), Structural (class/object composition), and Behavioral (object collaboration and responsibility assignment). All three categories are foundational — the impact label reflects the impact of applying the right pattern when the situation fits, not a global ranking between categories. In Python many of these patterns shrink to a language feature; each reference leads with the idiomatic form and keeps the class-based GoF structure only where it earns its weight.
---
1. Creational Patterns (creational)
Impact: HIGH Description: Five patterns that decouple client code from the concrete classes it instantiates. Apply when object construction is non-trivial, varies by configuration, or risks tight coupling to specific classes. In Python they often reduce to a registry dict, a @classmethod alternative constructor, a keyword-only @dataclass, copy.deepcopy/dataclasses.replace, or a module-level instance — reach for the class hierarchy only when you need polymorphic creation or extension points.
2. Structural Patterns (structural)
Impact: HIGH Description: Seven patterns that compose classes and objects into larger structures while keeping the structure flexible and the parts substitutable. Apply when integrating incompatible APIs, building tree-shaped models, attaching responsibilities at runtime, hiding subsystem complexity, or controlling access to expensive resources. Python leans on duck typing and typing.Protocol, __getattr__ delegation, functools.wraps decorators, __slots__/functools.lru_cache sharing, and cached_property rather than deep wrapper hierarchies.
3. Behavioral Patterns (behavioral)
Impact: HIGH Description: Ten patterns that distribute responsibility between objects and define how they communicate. Apply when behavior must vary at runtime, when responsibilities pass through a sequence of handlers, when state changes must propagate to many listeners, or when an algorithm's skeleton is fixed but specific steps vary. Python's first-class functions, generators, functools.singledispatch, and structural pattern matching (match) collapse several of these to a few lines — keep the GoF object form only when you need stored state, identity, or pluggable extension.
Use Chain of Responsibility to Pass Requests Through Handlers
Pattern intent: pass a request along a sequence of handlers; each either handles it (and may stop the chain) or passes it on. In Python the chain is most naturally a list of callables iterated in order — no linked-list plumbing needed unless handlers carry state.
Shapes to recognize
- A pipeline of checks: authenticate → rate-limit → validate → authorize, applied in order
- A deeply nested
if/elifcascade where each branch guards the next step - Middleware-style processing where steps should be reorderable or pluggable
- "I want to add a new check without touching the existing ones, and control its position"
Problem
An API request must pass authentication, payload validation, and an authorization limit before processing. Hardcoding these as nested if blocks fixes their order, forces every check to know the next, and makes inserting or reordering a step a risky edit.
Solution
Represent each step as a handler that returns None to pass the request along or a result to stop the chain. Drive the request through an ordered list of handlers; the first to return a non-None result short-circuits.
Incorrect (nested cascade hardcodes order and coupling):
def handle(req):
if req.user != "admin":
return "401 unauthenticated"
else:
if "amount" not in req.payload:
return "400 missing amount"
else:
if req.payload["amount"] > 1000: # adding a step means nesting deeper
return "403 over limit"
return "200 ok"Correct (ordered list of handlers; first non-None stops the chain):
from dataclasses import dataclass
from typing import Callable
@dataclass
class Request:
user: str
payload: dict
# A handler returns None to pass the request on, or a string to stop the chain.
Handler = Callable[[Request], str | None]
def authenticate(req: Request) -> str | None:
return None if req.user == "admin" else "401 unauthenticated"
def check_payload(req: Request) -> str | None:
return None if "amount" in req.payload else "400 missing amount"
def authorize(req: Request) -> str | None:
return "403 over limit" if req.payload["amount"] > 1000 else None
def handle(req: Request, chain: list[Handler]) -> str:
for link in chain:
result = link(req)
if result is not None: # a handler took responsibility
return result
return "200 ok"
chain = [authenticate, check_payload, authorize] # reorder/insert freely
print(handle(Request("admin", {"amount": 500}), chain))
print(handle(Request("guest", {"amount": 500}), chain))Output:
200 ok
401 unauthenticatedWhen to use
- More than one object may handle a request and the handler isn't known up front
- You want to process a request through a configurable, reorderable sequence of steps
- The set of handlers and their order should change at runtime
When NOT to use
- Exactly one handler always applies — call it directly
- All steps must always run regardless — a plain sequence of function calls is clearer than a chain
- The order is fixed and short — nesting two checks is fine without the abstraction
Implementation Steps
1. Define the request object the handlers operate on 2. Adopt a handler contract: return None to continue, or a result to stop 3. Write each step as a small handler function 4. Drive the request through an ordered list, stopping on the first non-None result 5. For stateful handlers, use linked objects with a set_next method instead of a flat list
Pros
- Decouples senders from receivers; handlers don't know each other
- Reorder, insert, or remove steps by editing the list (Open/Closed)
- Each handler has a single responsibility and is independently testable
Cons
- A request may fall through unhandled if no handler claims it
- Debugging is harder when it's unclear which handler acted
- Long chains can hurt performance and obscure control flow
Related Patterns
- Decorator — also a chain of wrappers, but every wrapper runs; CoR may stop early
- Command — handlers can be Command objects; CoR routes, Command reifies the request
- Composite — a CoR often runs over a Composite tree (event bubbling)
- Mediator — centralizes communication in a hub; CoR threads it through a sequence
Reference: refactoring.guru/design-patterns/chain-of-responsibility/python
Use Command to Turn Requests into Stand-Alone Objects
Pattern intent: turn a request into a stand-alone object carrying everything needed to perform it later — so it can be queued, logged, or undone. In Python a plain callable or closure is the lightest command; pair it with an inverse closure when you need undo.
Shapes to recognize
- The same action triggered from a button, a shortcut, and a menu — invocation logic duplicated each place
- A need for undo/redo, macro recording, deferred execution, or a job queue
- "I want to store an action now and run (or reverse) it later"
- A history stack of operations the user can step back through
Problem
A text editor exposes "append text" from a toolbar button, a keyboard shortcut, and a script API. Each entry point re-implements the call, and there's no clean way to add undo without scattering the inverse logic everywhere.
Solution
Capture each action as a command bundling a do and its inverse undo. An invoker runs commands and records them on a history stack; undo pops and reverses. The invoker never knows what the command does — only that it can be executed and undone.
Incorrect (invoker calls receiver logic directly; duplicated, no undo):
class Toolbar:
def __init__(self, editor):
self.editor = editor
def on_click(self, text):
self.editor.text += text # the shortcut handler and script API repeat this;
# nothing records it, so undo is impossible
Correct (commands bundle do/undo; a history runs and reverses them):
from dataclasses import dataclass
from typing import Callable
@dataclass
class Editor:
text: str = ""
@dataclass
class Command:
do: Callable[[], None]
undo: Callable[[], None]
class History: # the invoker
def __init__(self) -> None:
self._stack: list[Command] = []
def run(self, cmd: Command) -> None:
cmd.do()
self._stack.append(cmd)
def undo(self) -> None:
if self._stack:
self._stack.pop().undo()
editor, history = Editor(), History()
def append(text: str) -> Command: # a factory closing over the receiver
before = editor.text
return Command(do=lambda: setattr(editor, "text", editor.text + text),
undo=lambda: setattr(editor, "text", before))
history.run(append("hello "))
history.run(append("world"))
print(repr(editor.text))
history.undo()
print(repr(editor.text))Output:
'hello world'
'hello 'When to use
- You need undo/redo, macro recording, queueing, scheduling, or deferred execution
- You want to decouple the object that triggers an operation from the one that performs it
- The same operation is invoked from several places and you want one definition
When NOT to use
- The action runs immediately and never needs undo/queueing — a direct call or a plain function is enough
- You only need to pass behavior around — a bare
Callableorfunctools.partialis the minimal command - Bundling do/undo adds ceremony that a simple function call would avoid
Implementation Steps
1. Decide the command contract — a callable, or an object with do/undo 2. Write factory functions that close over the receiver and capture the inverse state for undo 3. Give the invoker a history stack; push each executed command 4. Implement undo by popping the stack and calling the command's inverse 5. Reuse the same command objects across every UI surface and the script API
Pros
- Decouples invoker from receiver (Single Responsibility)
- Enables undo/redo, queues, logging, and macros by treating actions as data
- New commands arrive without changing the invoker (Open/Closed)
Cons
- Adds a layer between caller and action
- Capturing correct undo state can be subtle (snapshot before, not after)
Related Patterns
- Memento — stores the state a command needs to undo itself
- Chain of Responsibility — handlers can be commands routed through a chain
- Strategy — both wrap behavior; Command represents a request, Strategy an interchangeable algorithm
- Observer — commands are often dispatched in response to observed events
Use Iterator to Traverse Collections Without Exposing Their Internals
Pattern intent: traverse a collection without exposing its underlying representation. Python builds this into the language: implement __iter__ (ideally as a generator using yield) and the object works with for, list(), sum(), in, unpacking, and every itertools helper.
Shapes to recognize
- A custom collection (tree, graph, ring buffer, paginated API) clients must walk
- Callers reaching into
.children/._itemsand writing their own recursion - The same traversal logic (depth-first walk, page fetching) copied in several places
- "I want
for x in my_structureto just work, hiding how it's stored"
Problem
A tree exposes its children list, so every caller that needs to walk it writes its own recursion over the internal structure. Change the storage and all that traversal code breaks; two callers walk it inconsistently.
Solution
Implement __iter__ as a generator that yields elements in traversal order and uses yield from to recurse. Callers use ordinary for/list() and never see the internal layout; each for gets a fresh, independent traversal.
Incorrect (callers recurse over exposed internals):
def walk(node, out):
out.append(node.value)
for child in node.children: # caller knows it's a list of children
walk(child, out) # every call site re-implements this
return outCorrect (`__iter__` generator hides the structure):
from collections.abc import Iterator
from dataclasses import dataclass, field
@dataclass
class TreeNode:
value: int
children: list["TreeNode"] = field(default_factory=list)
def __iter__(self) -> Iterator[int]:
yield self.value # depth-first, computed lazily
for child in self.children:
yield from child # delegate to the child's own __iter__
tree = TreeNode(1, [TreeNode(2, [TreeNode(4)]), TreeNode(3)])
print(list(tree)) # for, sum, any, unpacking all work now
print(4 in tree, sum(tree))Output:
[1, 2, 4, 3]
True 10When to use
- A collection's internal structure should stay hidden from callers
- You want several independent traversals over the same collection
- The traversal is non-trivial (tree, graph, lazy/paginated source) and shouldn't be duplicated
When NOT to use
- The data is already a
list/dict/set— iterate it directly, don't wrap it - Callers legitimately need index access and mutation — an iterator hides what they need
- A one-off traversal in a single place — a local generator function is enough
Implementation Steps
1. Decide the traversal order (depth-first, breadth-first, sorted, paginated) 2. Implement __iter__ as a generator that yields elements in that order 3. Use yield from to delegate into sub-collections recursively 4. Add a separate generator method per order if you need more than one (e.g., breadth_first()) 5. Rely on the protocol — for, list(), sum(), in, and itertools all work for free
Pros
- Hides representation; callers depend only on the iterator protocol (Single Responsibility)
- Generators make each traversal lazy and independent
- Eliminates duplicated traversal code and works with the whole standard library
Cons
- For trivially simple collections, a custom iterator is overkill
- Generator state is single-pass; you re-call
__iter__for a fresh walk
Related Patterns
- Composite — Iterators are the natural way to traverse Composite trees
- Visitor — pair an Iterator (traversal) with a Visitor (operation at each node)
- Factory Method —
__iter__is a factory method that produces the iterator - Memento — can capture an iterator's position to resume traversal
Use Mediator to Replace Many-to-Many Coupling with a Hub
Pattern intent: reduce chaotic dependencies between objects by having them communicate through a single mediator instead of referring to each other directly. Components know only the mediator; the mediator holds the coordination logic.
Shapes to recognize
- UI widgets that each hold references to several others to keep state in sync
- N components wired into an N x N web of direct references
- Coordination logic ("when the checkbox toggles, enable the field, hide the button") smeared across components
- "Every widget references every other widget and I can't reuse any of them in isolation"
Problem
A form's checkbox enables a text field, which gates a submit button, which updates a label. If each widget references the others directly, they form a tangled mesh: none can be reused alone, and a layout change ripples through every widget.
Solution
Give each component a reference to one mediator and have it report events there. The mediator holds the coordination rules and drives the other components. Components no longer reference each other — only the hub.
Incorrect (components reference each other directly — N x N mesh):
class Checkbox:
def __init__(self, field, button, label): # knows three peers
self.field, self.button, self.label = field, button, label
def toggle(self):
self.field.enabled = True
self.button.enabled = True # coordination duplicated in every widget
self.label.text = "ready"Correct (components talk to one mediator that coordinates them):
from typing import Protocol
class Mediator(Protocol):
def notify(self, sender: str, event: str) -> None: ...
class Checkbox:
def __init__(self, mediator: Mediator) -> None:
self._mediator = mediator
self.checked = False
def toggle(self) -> None:
self.checked = not self.checked
self._mediator.notify("checkbox", "toggled") # report, don't coordinate
class TextField:
def __init__(self) -> None:
self.enabled = False
class SubscribeForm: # the mediator
def __init__(self) -> None:
self.checkbox = Checkbox(self)
self.email = TextField()
def notify(self, sender: str, event: str) -> None:
if sender == "checkbox" and event == "toggled":
self.email.enabled = self.checkbox.checked # all rules live here
form = SubscribeForm()
form.checkbox.toggle()
print(form.checkbox.checked, form.email.enabled)Output:
True TrueWhen to use
- Components are tightly coupled by mutual references and hard to reuse independently
- Coordination logic is duplicated across components
- Changing one component forces edits in many others
When NOT to use
- Only two components interact — a direct reference is simpler than a hub
- The mediator would just forward calls with no real coordination — it adds nothing
- Coordination is naturally one-to-many notification — that is Observer
Implementation Steps
1. Identify the tangle of components that reference each other 2. Declare a mediator Protocol with a notify(sender, event) method 3. Give each component a reference to the mediator and have it report events instead of acting on peers 4. Move all cross-component coordination rules into the mediator's notify 5. Construct the components through the mediator so the wiring lives in one place
Pros
- Decouples components into a hub-and-spoke shape (N x 1 instead of N x N)
- Coordination logic is centralized and easy to change
- Components become reusable in isolation (Single Responsibility)
Cons
- The mediator can grow into a god object that knows everything
- Centralizing logic can make the hub a complexity magnet
Related Patterns
- Observer — one-to-many notification; Mediator is many-to-many coordination through a hub
- Facade — simplifies a subsystem one-directionally; Mediator enables two-way component talk
- Command — components may send commands to the mediator rather than raw events
- Singleton — a mediator is frequently a single shared instance
Use Memento to Snapshot State Without Breaking Encapsulation
Pattern intent: capture an object's internal state so it can be restored later, without exposing that state to the code holding the snapshot. In Python a frozen dataclass makes an immutable, opaque memento; the originator produces and consumes it, while the caretaker only stores it.
Shapes to recognize
- Undo/redo, transactional rollback, checkpoints, or "restore previous version"
- A history list that needs to store past states without understanding them
- Tempted to add public getters/setters for every field just so something else can snapshot it
- "I need to save and restore this object's state without leaking its internals"
Problem
A text editor supports undo. The history needs prior states, but exposing the editor's content and cursor as public mutable fields so the history can read and rewrite them breaks encapsulation and lets the history corrupt the editor.
Solution
The originator (editor) produces a memento — a frozen snapshot — and is the only code that can interpret it. A caretaker (history) holds mementos as opaque tokens and hands one back to restore. Encapsulation stays intact because the caretaker never reads the memento's fields.
Incorrect (caretaker reads/writes the originator's internals):
class History:
def save(self, editor):
# Reaches into private state; adding a field means editing every save site,
# and the history can now corrupt the editor.
self.snapshots.append((editor._content, editor._cursor))
def restore(self, editor):
editor._content, editor._cursor = self.snapshots.pop()Correct (frozen memento; originator saves/restores, caretaker only stores):
from dataclasses import dataclass
@dataclass(frozen=True)
class EditorState: # the memento: immutable, opaque to the caretaker
content: str
cursor: int
class Editor: # the originator
def __init__(self) -> None:
self._content, self._cursor = "", 0
def type(self, text: str) -> None:
self._content += text
self._cursor = len(self._content)
def save(self) -> EditorState:
return EditorState(self._content, self._cursor)
def restore(self, state: EditorState) -> None:
self._content, self._cursor = state.content, state.cursor
def __str__(self) -> str:
return f"{self._content!r}@{self._cursor}"
class History: # the caretaker: stores, never inspects
def __init__(self) -> None:
self._states: list[EditorState] = []
def push(self, state: EditorState) -> None:
self._states.append(state)
def pop(self) -> EditorState:
return self._states.pop()
editor, history = Editor(), History()
editor.type("hello")
history.push(editor.save())
editor.type(" world")
print(editor)
editor.restore(history.pop())
print(editor)Output:
'hello world'@11
'hello'@5When to use
- You need snapshots for undo/redo, rollback, or checkpointing
- You want to restore prior state without exposing the object's internals
- Direct field access for snapshotting would violate encapsulation
When NOT to use
- The object is small and already immutable — store copies directly
- A full
copy.deepcopyof the originator is acceptable and simpler than a tailored memento - State changes constantly and snapshots would be huge — consider command-based undo instead
Implementation Steps
1. Identify the originator state that must be saved and restored 2. Define a frozen dataclass memento holding exactly that state 3. Give the originator a save() returning a memento and a restore(memento) 4. Give the caretaker a stack/list that stores mementos without reading them 5. For complex graphs, implement __getstate__/__setstate__ or use copy.deepcopy
Pros
- Snapshots without violating the originator's encapsulation
- The caretaker stays simple — it stores opaque tokens
- A frozen dataclass memento can't be tampered with after capture
Cons
- Many or large mementos consume memory
- Caretakers must manage memento lifetime (when to discard old ones)
Related Patterns
- Command — uses mementos to implement undo; Command represents the action, Memento the state
- Prototype —
copy.deepcopyis a simpler snapshot when encapsulation isn't a concern - Iterator — a memento can capture iteration position to resume later
- State — mementos can record which state an object was in
Use Observer to Broadcast State Changes to Many Subscribers
Pattern intent: define a one-to-many dependency so that when one object changes state, all its dependents are notified automatically. In Python a subscriber is usually just a callback callable; the subject keeps a list of them and invokes each on change, often from a property setter.
Shapes to recognize
- Many objects must react when one object changes — UI bindings, live dashboards, dataflow
- Polling: code repeatedly checks "did it change yet?" instead of being told
- A publisher hard-coding calls to each concrete consumer it must update
- "When this value changes, notify everyone who cares — and I shouldn't know who they are"
Problem
A price feed must update a chart, trigger alerts, and append to a log whenever the price changes. If the feed calls each consumer by name, it's coupled to all of them and can't gain a new consumer without an edit.
Solution
Let the subject keep a list of subscriber callbacks and expose subscribe. On change — naturally inside a property setter — it calls every subscriber. The subject knows nothing about what subscribers do; subscribers register and unregister freely.
Incorrect (publisher hard-codes each concrete consumer):
class PriceFeed:
def __init__(self, chart, alerts, log):
self.chart, self.alerts, self.log = chart, alerts, log
def set_price(self, value):
self.chart.redraw(value) # adding a consumer means editing this method
self.alerts.check(value)
self.log.append(value)Correct (subject notifies a list of subscriber callbacks):
from typing import Callable
Subscriber = Callable[[float], None]
class PriceFeed: # the subject / publisher
def __init__(self) -> None:
self._subscribers: list[Subscriber] = []
self._price = 0.0
def subscribe(self, fn: Subscriber) -> Callable[[], None]:
self._subscribers.append(fn)
return lambda: self._subscribers.remove(fn) # returns an unsubscribe handle
@property
def price(self) -> float:
return self._price
@price.setter
def price(self, value: float) -> None:
self._price = value
for fn in self._subscribers: # notify everyone, knowing nothing about them
fn(value)
feed = PriceFeed()
feed.subscribe(lambda p: print(f"chart: {p}"))
feed.subscribe(lambda p: print("alert!" if p > 100 else "ok"))
feed.price = 105Output:
chart: 105
alert!When to use
- A change in one object must propagate to an open-ended set of others
- The publisher should not depend on the concrete types of its subscribers
- You are building events, reactive bindings, pub/sub, or dataflow
When NOT to use
- There is exactly one fixed dependent — a direct call is simpler
- Notification order or cascading updates must be tightly controlled — observers fire in registration order
- A mature event library (signals, an event bus) already fits — don't reinvent it
Implementation Steps
1. Give the subject a list of subscriber callables and a subscribe method 2. Return an unsubscribe handle from subscribe so listeners can detach 3. Trigger notification where state changes — a property setter is the natural seam 4. Iterate subscribers and call each with the new value/event 5. Keep subscribers ignorant of each other and the subject ignorant of subscriber types
Pros
- Open/Closed: add subscribers without changing the publisher
- Loose coupling between publisher and subscribers
- Foundational for events, reactive UI, and dataflow
Cons
- Subscribers are notified in an unspecified order
- Forgotten unsubscriptions cause memory leaks (consider
weakrefcallbacks) - Cascading notifications can be hard to trace and debug
Related Patterns
- Mediator — centralizes many-to-many talk; Observer is one publisher to many subscribers
- Command — observed events often dispatch commands
- Chain of Responsibility — both relay events; CoR may stop, Observer notifies all
- Singleton — a global event bus subject is frequently a singleton
Use State to Alter Behavior When Internal State Changes
Pattern intent: let an object change its behavior when its internal state changes, as if it changed class. Each state is an object that implements the context's actions for that state and decides the next transition. In Python the states share a Protocol; the context delegates to the current one.
Shapes to recognize
- A class that is really a state machine, with
if self.status == "x"repeated in every method - The same status checks duplicated across methods, easy to update inconsistently
- Transitions tangled into business logic
- "Adding a new status means hunting through every method to add another branch"
Problem
A media player behaves differently when locked, ready, or playing. Encoding this as if self.status == ... blocks duplicates the status check in every method, and adding a "buffering" state means editing all of them — a frequent source of inconsistency.
Solution
Model each state as an object implementing the actions, where each action performs the behavior and sets the context's next state. The context delegates calls to its current state object — no status conditionals anywhere.
Incorrect (status conditionals duplicated across methods):
class Player:
def __init__(self):
self.status = "ready"
def press_play(self):
if self.status == "locked":
return
elif self.status == "ready":
self.status = "playing"
elif self.status == "playing": # every other method repeats this ladder
self.status = "ready"Correct (polymorphic state objects own behavior and transitions):
from typing import Protocol
class State(Protocol):
def press_play(self, player: "Player") -> None: ...
class Locked:
def press_play(self, player: "Player") -> None:
print("locked; ignoring")
class Ready:
def press_play(self, player: "Player") -> None:
print("playing")
player.state = Playing() # the state decides the transition
class Playing:
def press_play(self, player: "Player") -> None:
print("pausing")
player.state = Ready()
class Player:
def __init__(self) -> None:
self.state: State = Ready()
def press_play(self) -> None:
self.state.press_play(self) # delegate; no status ladder
player = Player()
player.press_play()
player.press_play()Output:
playing
pausingWhen to use
- An object behaves differently depending on a state, with many state-dependent methods
- Conditionals on a status field are duplicated across methods
- States and transitions change often enough that a class per state pays off
When NOT to use
- There are only two states and one or two methods — a boolean and an
ifis clearer - Transitions are simple and fixed — an
enumplus a transitiondictis lighter than state classes - The states share almost no behavior — separate objects add ceremony for little gain
Implementation Steps
1. Identify the context and the actions whose behavior depends on state 2. Declare a state Protocol with one method per state-dependent action 3. Implement one class per state; each method performs behavior and sets the next state 4. Give the context a state field and delegate each action to it 5. For trivial machines, prefer an enum with a dict of allowed transitions
Pros
- Open/Closed: a new state is a new class, not edits across every method
- Removes duplicated status conditionals (Single Responsibility per state)
- Transition logic is localized in the states themselves
Cons
- More classes than a simple conditional for small machines
- Transition logic spread across state classes can be hard to see as a whole
Related Patterns
- Strategy — same composition shape; State objects know each other and self-transition, Strategy objects are independent
- Bridge — also delegates to a swapped object, but to vary an implementation dimension
- Singleton — stateless state objects are often shared singletons
- Memento — can snapshot which state the context is in
Use Strategy to Make Algorithms Interchangeable at Runtime
Pattern intent: define a family of interchangeable algorithms and let the caller choose one at runtime. Because Python functions are first-class, a strategy is usually just a `Callable` passed in — no Strategy interface or concrete-strategy classes required.
Shapes to recognize
- A method that branches on
mode/kind/typeto pick an algorithm - Several variants of the same operation (sort, route, price, compress) chosen at runtime
- A class growing every time a new algorithm variant is added
- "I want to swap how this is computed without subclassing or editing the caller"
Problem
A navigation app supports car and walking routes, with cyclist and transit planned. Encoding each as a branch inside Navigator swells the class, raises bug risk, and makes every new mode a merge-conflict-prone edit to the same method.
Solution
Make the algorithm a Callable the context holds and delegates to. Each variant is a plain function; switching is one assignment. The context never knows which algorithm it runs.
Incorrect (algorithm-selection conditional inside the context):
class Navigator:
def build(self, mode: str, start: str, end: str) -> list[str]:
if mode == "car":
return [start, "highway", end]
elif mode == "walking":
return [start, "park path", end]
# Adding "cyclist" edits this method (and every other shaped like it).
raise ValueError(mode)Correct (interchangeable strategy functions):
from collections.abc import Callable
Route = Callable[[str, str], list[str]]
def car_route(start: str, end: str) -> list[str]:
return [start, "highway", end]
def walking_route(start: str, end: str) -> list[str]:
return [start, "park path", end]
class Navigator:
def __init__(self, strategy: Route) -> None:
self.strategy = strategy
def build(self, start: str, end: str) -> list[str]:
return self.strategy(start, end) # delegate to the chosen algorithm
nav = Navigator(car_route)
print(nav.build("home", "work"))
nav.strategy = walking_route # swap at runtime: one assignment
print(nav.build("home", "work"))Output:
['home', 'highway', 'work']
['home', 'park path', 'work']When to use
- You have several variants of an algorithm and want to choose one at runtime
- A class has large conditionals selecting an algorithm
- You want each algorithm isolated and independently testable
When NOT to use
- There's one stable algorithm that rarely changes — a function call suffices
- The algorithm must be fixed at definition time and never swap — inheritance/Template Method is enough
- The strategy needs significant configuration or state — then a small class is warranted over a bare function
Implementation Steps
1. Identify the algorithm in the context that varies 2. Define the strategy as a Callable type alias capturing its signature 3. Write each variant as a plain function matching that signature 4. Give the context a strategy attribute and delegate to it 5. Let callers pass or reassign the strategy; reach for a class only if the strategy needs state
Pros
- Swap algorithms at runtime with a single assignment
- Each algorithm is isolated and testable in isolation
- Replaces inheritance with composition (Open/Closed for new strategies)
Cons
- For a few stable algorithms, the indirection adds little
- Callers must understand the variants to choose correctly
Related Patterns
- State — same shape; State objects self-transition and know each other, Strategy variants are independent
- Template Method — varies steps via inheritance (compile-time); Strategy swaps the whole algorithm (runtime)
- Command — both wrap behavior; Command represents a request, Strategy an algorithm
- Decorator — changes the outer skin; Strategy changes the inner algorithm
Use Template Method to Fix an Algorithm Skeleton and Let Subclasses Override Steps
Pattern intent: define the skeleton of an algorithm in a base method, deferring some steps to subclasses so they vary the steps without changing the structure. In Python this is an ABC with a concrete template method calling @abstractmethod steps — or, when no shared state is involved, a higher-order function taking the varying steps as callables.
Shapes to recognize
- Several classes run the same overall sequence with a few differing steps
- Copy-pasted scaffolding (open → process → close) across siblings, differing only in the middle
- Client conditionals that switch on subtype to run slightly different versions
- "These all follow the same recipe; only one or two steps change"
Problem
CSV and JSON reports both load data, format it, and wrap it with a title — the same sequence — but each duplicates the whole skeleton to vary just the load and format steps. Fixing the shared sequence means editing every report class.
Solution
Put the fixed sequence in a base-class template method that calls abstract steps. Subclasses override only the steps that vary; the skeleton lives in one place. Optional hook methods provide overridable defaults.
Incorrect (each class duplicates the whole skeleton):
class CsvReport:
def generate(self):
rows = ["a,1", "b,2"] # load
body = "\n".join(rows) # format
return f"=== CSV Report ===\n{body}" # wrap — duplicated below
class JsonReport:
def generate(self):
rows = ['{"a":1}', '{"b":2}'] # only load/format differ; wrap is copy-pasted
body = ",".join(rows)
return f"=== JSON Report ===\n{body}"Correct (base template method fixes the skeleton; subclasses fill steps):
from abc import ABC, abstractmethod
class Report(ABC):
def generate(self) -> str: # the template method: the fixed skeleton
body = self.format(self.load())
return f"=== {self.title()} ===\n{body}"
@abstractmethod
def load(self) -> list[str]: ...
@abstractmethod
def format(self, rows: list[str]) -> str: ...
def title(self) -> str: # hook: overridable default
return "Report"
class CsvReport(Report):
def load(self) -> list[str]:
return ["a,1", "b,2"]
def format(self, rows: list[str]) -> str:
return "\n".join(rows)
def title(self) -> str:
return "CSV Report"
print(CsvReport().generate())Output:
=== CSV Report ===
a,1
b,2Alternative (higher-order function when steps need no shared state):
from typing import Callable
def generate(load: Callable[[], list[str]],
fmt: Callable[[list[str]], str],
title: str = "Report") -> str:
return f"=== {title} ===\n{fmt(load())}"When to use
- Several classes share an algorithm structure that differs in a few steps
- You want to let subclasses extend only specific steps, not the whole algorithm
- You want to pull duplicated scaffolding into one place
When NOT to use
- The steps share no state — a higher-order function is simpler than an ABC hierarchy
- Behavior must change at runtime rather than be fixed by subtype — use Strategy
- Only one implementation exists — a plain function is enough
Implementation Steps
1. Break the algorithm into steps and identify which are shared vs. varying 2. Put the fixed sequence in a base-class template method 3. Declare varying steps as @abstractmethod; give overridable defaults as hook methods 4. Implement each subclass by overriding only the steps it changes 5. If no shared state exists, prefer a higher-order function taking step callables
Pros
- Hoists duplicated scaffolding into one place (Single Responsibility)
- Subclasses override only what varies; the skeleton stays consistent
- Hook methods offer optional extension points
Cons
- Limited by inheritance — behavior is fixed at class-definition time, not runtime
- A rigid skeleton can be awkward when a subclass needs to vary the sequence itself
- Many abstract steps make subclasses tedious to implement
Related Patterns
- Strategy — composition and runtime swap; Template Method uses inheritance and compile-time steps
- Factory Method — often a single step within a template method
- Bridge / Builder — combine with Template Method for staged construction or layered abstraction
Reference: refactoring.guru/design-patterns/template-method/python
Use Visitor to Add Operations to Class Hierarchies Without Modifying Them
Pattern intent: add new operations over a set of object types without modifying those types. The classic GoF form uses double dispatch (accept/visit); Python replaces it with functools.singledispatch, which dispatches one function on its first argument's type — or structural pattern matching (match) when all cases live in one function.
Shapes to recognize
- An AST, scene graph, or shape hierarchy with several node types
- New operations (export to XML, type-check, render, compute area) needed across all of them
- Each new operation currently means adding a method to every node class — N x M growth
- "I need several unrelated operations over a closed set of types I'd rather not edit"
Problem
A geometry library has Circle and Rectangle. Computing area, perimeter, and bounding box by adding a method to every shape for each operation spreads each operation across the hierarchy and forces editing every class whenever an operation is added.
Solution
Write each operation as a single @singledispatch function with a registered implementation per type. Adding an operation is a new function; the shape classes never change. match is the alternative when you prefer one function with exhaustive cases.
Incorrect (every new operation edits every node class):
class Circle:
def area(self): ...
def perimeter(self): ... # adding bounding_box() edits Circle AND Rectangle AND ...
class Rectangle:
def area(self): ...
def perimeter(self): ...Correct (`@singledispatch`: one function per operation, dispatched by type):
from dataclasses import dataclass
from functools import singledispatch
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
@singledispatch
def area(shape: object) -> float: # the operation lives outside the classes
raise NotImplementedError(f"no area for {type(shape).__name__}")
@area.register
def _(shape: Circle) -> float:
return 3.14159 * shape.radius ** 2
@area.register
def _(shape: Rectangle) -> float:
return shape.width * shape.height
shapes: list[object] = [Circle(1.0), Rectangle(2.0, 3.0)]
print([round(area(s), 2) for s in shapes]) # adding perimeter() touches no shape classOutput:
[3.14, 6.0]Alternative (structural pattern matching when one function suits all cases):
def area(shape: object) -> float:
match shape:
case Circle(radius=r):
return 3.14159 * r ** 2
case Rectangle(width=w, height=h):
return w * h
case _:
raise NotImplementedError(shape)When to use
- You must add many unrelated operations over a stable set of types
- You cannot or prefer not to edit the element classes for each new operation
- An operation should live in one place rather than be smeared across the hierarchy
When NOT to use
- The set of types changes often — every new type means updating every operation
- There is only one operation across a small hierarchy — a method on each type is simpler
- The operation needs private state of the elements that dispatch can't reach cleanly
Implementation Steps
1. Keep the element types as plain (data)classes — ideally dataclasses 2. Write each operation as a @singledispatch function with a fallback that raises 3. Register one implementation per concrete type with @operation.register 4. Call the operation as a normal function; dispatch picks the right implementation 5. Prefer match when you'd rather keep all cases in one exhaustive function
Pros
- Open/Closed for operations: add an operation without touching the types
- Each operation is isolated in one function (Single Responsibility)
singledispatchandmatchavoid the GoFaccept/visitboilerplate entirely
Cons
- Adding a new type requires updating every operation
- Free functions can't reach private element state the way methods can
singledispatchkeys on the first argument's runtime type only
Related Patterns
- Composite — Visitor commonly walks a Composite tree applying an operation per node
- Iterator — traverse with an Iterator while applying a Visitor at each element
- Command — both externalize behavior; Visitor dispatches across many element types
- Strategy — both inject behavior; Visitor selects by element type, Strategy by caller choice
Use Abstract Factory to Produce Families of Related Objects
Pattern intent: produce families of related objects (a button and a checkbox and a menu, all in one visual style) without binding callers to concrete classes, and guarantee the produced objects belong to the same family. In Python a "factory" is just an object whose methods build the parts — a small class implementing a Protocol, or even a frozen dataclass bundling constructors.
Shapes to recognize
- Several products that must vary together — Victorian chair + sofa + table, or Windows button + checkbox
- Repeated
if style == "dark": Button = DarkButton; Checkbox = DarkCheckboxsetup at many call sites - A risk that callers mix families (a light button with a dark checkbox) and nobody notices
- "I switch one config flag and a whole coordinated set of objects must change"
Problem
A cross-platform UI builds buttons and checkboxes. Pick the platform once, and every widget must come from that platform's family. Hard-coding WinButton() and WinCheckbox() at each site means one missed branch produces a macOS checkbox inside a Windows dialog.
Solution
Declare a factory Protocol with one creator method per product. Each variant is a concrete factory that returns its own family. The app receives one factory and asks it for every part — the family can never be mixed because a single object makes all of them.
Incorrect (each call site re-decides the variant, so families drift):
def build_form(platform: str):
button = WinButton() if platform == "win" else MacButton()
# A second site forgets the check and hard-codes WinCheckbox() — families now mix.
checkbox = WinCheckbox()
return button, checkboxCorrect (one factory makes a coherent family):
from typing import Protocol
class Button(Protocol):
def render(self) -> str: ...
class Checkbox(Protocol):
def render(self) -> str: ...
class GUIFactory(Protocol):
def create_button(self) -> Button: ...
def create_checkbox(self) -> Checkbox: ...
class WinButton:
def render(self) -> str: return "[ Windows button ]"
class WinCheckbox:
def render(self) -> str: return "[x] Windows checkbox"
class MacButton:
def render(self) -> str: return "( macOS button )"
class MacCheckbox:
def render(self) -> str: return "[x] macOS checkbox"
class WinFactory:
def create_button(self) -> Button: return WinButton()
def create_checkbox(self) -> Checkbox: return WinCheckbox()
class MacFactory:
def create_button(self) -> Button: return MacButton()
def create_checkbox(self) -> Checkbox: return MacCheckbox()
def build_form(factory: GUIFactory) -> str:
# Receives ONE factory; every part is guaranteed same-family.
return f"{factory.create_button().render()} {factory.create_checkbox().render()}"
factories = {"win": WinFactory(), "mac": MacFactory()}
print(build_form(factories["mac"]))Output:
( macOS button ) [x] macOS checkboxWhen to use
- Your code must work with several families of related products and must not mix them
- The concrete family is chosen once (startup, config, OS detection) and reused everywhere
- You want to enforce that a set of products is mutually compatible by construction
When NOT to use
- There is only one family — the abstraction is pure overhead; build the parts directly
- Products are unrelated and never need to match — a per-product factory (or registry) is simpler
- The family is two trivial values — a frozen
dataclassof constructors reads more plainly than a class hierarchy
Implementation Steps
1. Map the product matrix: rows are product types (button, checkbox), columns are variants (win, mac) 2. Declare a Protocol for each product type 3. Declare a factory Protocol with one create_* method per product type 4. Implement one concrete factory per variant, returning that variant's products 5. Resolve the factory once (a dict keyed by variant) and thread it through; callers ask it for parts
Pros
- Products from one factory are guaranteed compatible
- Swapping the whole family is a one-line factory change
- Concrete product classes stay isolated from client code (Single Responsibility, Open/Closed)
Cons
- Adding a new product type changes the factory
Protocoland every concrete factory - More indirection than warranted when only one family will ever exist
Related Patterns
- Factory Method — Abstract Factory is a set of factory methods grouped to build a family
- Builder — Builder assembles one complex object step by step; Abstract Factory returns families of finished parts
- Singleton — the chosen concrete factory is commonly a module-level singleton
- Prototype — a factory can clone prototypes instead of instantiating classes
Reference: refactoring.guru/design-patterns/abstract-factory/python
Use Builder to Construct Complex Objects Step by Step
Pattern intent: separate the construction of a complex object from its representation so the same steps can build different results. In Python the most common Builder motivation — too many constructor parameters — is solved outright by a keyword-only `@dataclass`. Reach for a fluent/staged builder only when construction is genuinely multi-step, ordered, or produces different representations.
Shapes to recognize
- A constructor with 10+ parameters, most optional, callers passing
None, None, True, None - Subclasses created only to bake in a fixed parameter combination (
HouseWithPoolAndGarage) - Object assembly that proceeds in stages with validation between steps
- "I want readable construction and I keep forgetting which positional argument is which"
Problem
A House needs walls, roof, optional pool, optional garden, optional garage. A single constructor balloons to a dozen positional parameters; callers can't tell True, False, True apart, and every new combination tempts a new subclass.
Solution
For the "many optional fields" case, a keyword-only dataclass names every argument and supplies defaults — construction is self-documenting and combinations cost nothing. For staged assembly with validation or alternate outputs, use a fluent builder whose methods return self.
Incorrect (telescoping constructor — positional soup):
class House:
def __init__(self, walls, roof, has_pool=False, has_garden=False,
has_garage=False, floors=1, windows=4):
...
# Unreadable at the call site; which bool is which?
house = House("brick", "tile", True, False, True, 2, 12)Correct (keyword-only dataclass: named, defaulted, order-free):
from dataclasses import dataclass
@dataclass(kw_only=True)
class House:
walls: str
roof: str
floors: int = 1
windows: int = 4
has_pool: bool = False
has_garden: bool = False
has_garage: bool = False
house = House(walls="brick", roof="tile", floors=2, windows=12, has_garage=True)
print(house)Output:
House(walls='brick', roof='tile', floors=2, windows=12, has_pool=False, has_garden=False, has_garage=True)Alternative (fluent builder when assembly is staged or yields different representations):
class HouseBuilder:
def __init__(self) -> None:
self._parts: dict[str, object] = {}
def walls(self, material: str) -> "HouseBuilder":
self._parts["walls"] = material
return self # return self → chainable
def pool(self) -> "HouseBuilder":
self._parts["pool"] = True
return self
def build(self) -> House:
return House(walls=self._parts.get("walls", "brick"), roof="tile",
has_pool=self._parts.get("pool", False))
villa = HouseBuilder().walls("stone").pool().build()When to use
- A constructor has many parameters, most optional (use the dataclass form)
- Construction is multi-step, ordered, or needs validation between steps (use the fluent form)
- The same construction sequence must produce different representations (e.g., a director driving JSON vs. XML output)
When NOT to use
- The object has a handful of required fields — a plain
@dataclassor constructor is enough - There is exactly one representation and no staging — a keyword-only dataclass already wins
- You reach for a builder out of habit; in Python keyword arguments make most builders unnecessary
Implementation Steps
1. Start with @dataclass(kw_only=True) and defaults — this resolves the common case 2. If construction is staged, define a builder class accumulating parts in instance state 3. Return self from each step method to allow chaining 4. Expose a terminal build() that validates and returns the finished product 5. Optionally add a director that encapsulates a common construction recipe over the builder
Pros
- Construction is readable and order-independent (keyword arguments)
- New optional fields/combinations cost nothing — no subclass explosion
- A fluent builder isolates assembly logic and can enforce step ordering and validation
Cons
- A fluent builder is more code than a dataclass and is rarely needed in Python
- A director adds another layer that only pays off when recipes are reused
Related Patterns
- Abstract Factory — returns families of finished products; Builder assembles one product over steps
- Factory Method — a builder may use a factory method to pick the part implementations
- Prototype — clone a fully built object instead of re-running the construction steps
- Composite — builders frequently assemble Composite trees
Use Factory Method to Decouple Object Creation from Concrete Classes
Pattern intent: define an interface for creating an object, but let the choice of concrete class be made elsewhere (a subclass or a registry). Callers consume products through a shared interface and never name the concrete type. In Python this usually collapses to a registry dict mapping a key to a product callable — no creator hierarchy required.
Shapes to recognize
- Code littered with
Truck(),Ship(),Drone()whose downstream handling is otherwise identical - An
if kind == ...: return X()ladder inside a constructor or helper that returns different classes - A library that wants users to plug in their own product types without editing library code
- "I want to choose what gets instantiated by a string/config key, not a hard-coded class"
Problem
A logistics app coupled to Truck struggles to add Ship: every site that builds a transport hard-codes the class, so each new mode (drone, freight) spreads the same conditional across the codebase and risks one branch drifting from the rest.
Solution
Make all products honor a common Protocol, then resolve the concrete class through a registry keyed by a string. A @register decorator lets new products opt in at import time — adding one is a new class, not an edit to the resolver.
Incorrect (caller branches on a string to pick a concrete class):
def make_transport(kind: str):
if kind == "truck":
return Truck()
elif kind == "ship":
return Ship()
# Adding "drone" forces an edit here AND in every other place shaped like this.
raise ValueError(kind)Correct (products register themselves; the factory resolves by key):
from typing import Protocol
class Transport(Protocol):
def deliver(self) -> str: ...
_TRANSPORTS: dict[str, type[Transport]] = {}
def register(name: str):
"""Class decorator that adds a transport to the registry at import time."""
def wrap(cls: type[Transport]) -> type[Transport]:
_TRANSPORTS[name] = cls
return cls
return wrap
@register("truck")
class Truck:
def deliver(self) -> str:
return "delivered by road in a box"
@register("ship")
class Ship:
def deliver(self) -> str:
return "delivered by sea in a container"
def create_transport(name: str) -> Transport:
try:
return _TRANSPORTS[name]()
except KeyError:
raise ValueError(f"unknown transport: {name!r}") from None
# Adding a Drone is one new class + @register("drone") — create_transport never changes.
for name in ("truck", "ship"):
print(create_transport(name).deliver())Output:
delivered by road in a box
delivered by sea in a containerAlternative (class-based creator when the creator owns shared business logic that consumes the product):
from abc import ABC, abstractmethod
class Logistics(ABC):
@abstractmethod
def create_transport(self) -> Transport: ...
def plan_delivery(self) -> str: # shared logic; varies only by product
return f"Planned: {self.create_transport().deliver()}"
class RoadLogistics(Logistics):
def create_transport(self) -> Transport:
return Truck()When to use
- The concrete type to build is unknown beforehand or chosen by config/plugin/string key
- You are building a framework and want third parties to extend the set of products
- A creator class holds business logic that consumes the product (use the class-based form)
When NOT to use
- The product set is fixed, small, and trivially constructed — call the constructor directly
- You only ever build one variant — a registry or creator hierarchy is dead weight
- You need a family of related products that must match — reach for Abstract Factory
Implementation Steps
1. Define a Protocol (or ABC) that all products implement 2. Create a module-level registry dict[str, type[Product]] 3. Add a @register(key) class decorator that records each product 4. Write create_<thing>(key) that looks up the class and instantiates it, raising on miss 5. Replace scattered constructor calls with the resolver; for a creator with shared logic, use an ABC with an abstract create_* method instead
Pros
- Decouples callers from concrete classes — they depend only on the
Protocol - New product types register without editing the resolver (Open/Closed)
- Centralizes construction, so cross-cutting concerns (pooling, logging) live in one place
Cons
- A registry adds indirection that a plain
dictof constructors or directnewwould not - Import-time registration means the product module must be imported for its key to exist
Related Patterns
- Abstract Factory — evolves from Factory Method when you need families of products that match
- Prototype — clone a configured instance instead of subclassing a creator
- Template Method — a factory method is often one step inside a template method
- Singleton — the registry/resolver itself is frequently a module-level singleton
Reference: refactoring.guru/design-patterns/factory-method/python
Use Prototype to Clone Objects Without Coupling to Concrete Classes
Pattern intent: copy existing objects without making the caller depend on their concrete classes. Python builds this in: copy.copy/copy.deepcopy clone any object, and dataclasses.replace produces a modified copy of a dataclass. You rarely write a clone() method — you override __copy__/__deepcopy__ only when default copying is wrong.
Shapes to recognize
- A hand-written "copy constructor" that lists every field — and breaks the moment a field is added
- Cloning needed for an object whose concrete class the caller shouldn't have to know
- A configured instance (default settings, pre-wired graph) that's expensive to rebuild from scratch
- "I need another one just like this, but with one value changed"
Problem
A pre-configured object — a chart with styling, scales, and a populated dataset — must be duplicated so a variant can be tweaked. A manual copy constructor has to know every field, reaches into nested mutable state by hand, and rots whenever the class gains a field.
Solution
Use copy.deepcopy to clone the whole object graph independent of its class, and dataclasses.replace when you want a copy with a few fields changed. Both work through a uniform interface, so callers never name the concrete type.
Incorrect (manual copy constructor — rots and shares nested state):
class Chart:
def __init__(self, title, series, axes):
self.title = title
self.series = series
self.axes = axes
def copy(self) -> "Chart":
# Forgot deepcopy: the clone shares `series`/`axes` with the original.
# Add a field later and this silently stops copying it.
return Chart(self.title, self.series, self.axes)Correct (`deepcopy` clones the graph; `replace` tweaks a copy):
import copy
from dataclasses import dataclass, field, replace
@dataclass
class Chart:
title: str
series: list[int] = field(default_factory=list)
axes: dict[str, str] = field(default_factory=dict)
original = Chart(title="Revenue", series=[1, 2, 3], axes={"x": "month"})
twin = copy.deepcopy(original) # fully independent clone, any class, any depth
twin.series.append(4) # does not touch `original.series`
variant = replace(original, title="Revenue (EU)") # copy with one field changed
print(original.series, twin.series)
print(variant.title, "| shares series with original:", variant.series is original.series)Output:
[1, 2, 3] [1, 2, 3, 4]
Revenue (EU) | shares series with original: TrueNote the contrast: deepcopy produces a fully independent clone, while dataclasses.replace is a shallow copy — any field you don't override (here series) is shared with the original. Use deepcopy (or override the mutable fields in the replace call) when you need the copy's nested state to be independent.
When to use
- You need copies of objects whose concrete class the caller should not depend on
- Building a fresh instance is costlier than copying a configured one
- You want a near-identical object with a few values changed (
dataclasses.replace)
When NOT to use
- The object is cheap and simple to construct directly — copying buys nothing
- The object holds non-copyable resources (open sockets, file handles, locks) — implement
__deepcopy__to handle them or avoid cloning - A shallow share is actually what you want —
copy.copyor a direct reference is clearer than deepcopy
Implementation Steps
1. Reach for copy.deepcopy(obj) for an independent clone of the whole graph 2. Reach for dataclasses.replace(obj, field=value) to copy-with-changes a dataclass 3. Use copy.copy(obj) when a shallow copy (shared nested objects) is intended 4. Override __deepcopy__/__copy__ only when default copying mishandles a field (resources, caches, back-references) 5. Drop any hand-written copy constructors that duplicate this for free
Pros
- Clone any object without knowing its concrete class
deepcopyhandles arbitrary nesting and cycles automaticallyreplacemakes copy-with-changes a single expression and keeps immutability intact
Cons
deepcopyis slow on large graphs and copies things you may not want copied- Objects holding external resources need custom
__deepcopy__or cannot be cloned safely
Related Patterns
- Abstract Factory — can use prototypes (clone) instead of instantiating concrete classes
- Factory Method — a factory may return a clone of a stored prototype
- Composite / Decorator — deep cloning a Composite tree relies on Prototype-style copying
- Memento — both snapshot state; Memento restores history, Prototype produces independent twins
Reference: refactoring.guru/design-patterns/prototype/python
Use Singleton to Guarantee a Single Shared Instance
Pattern intent: ensure a class has exactly one instance and provide one access point to it. Python already gives you this: a module is imported once, so a module-level object is a process-wide singleton, and functools.cache turns any factory into a lazily-initialized one. The classic __new__/metaclass machinery is rarely the right tool.
Shapes to recognize
- A config, logger, registry, or connection pool that must be shared across the program
- Bug reports where two parts of the system disagree because each built its own instance
- A module-level
_instance = Noneguarded by aget_instance()— a singleton in disguise - "I need one shared object, but I don't want a bare mutable global scattered around"
Problem
Two concerns at once: guarantee a single instance of a class managing a shared resource (pool, config), and give the program one place to reach it. A plain constructor always returns a fresh object, so two callers silently get two instances whose state drifts apart.
Solution
Prefer a module-level instance (created once at import) or a @functools.cache factory for lazy creation. Both give one shared instance and one named access point — and unlike a hidden global, both can be swapped in tests.
Incorrect (custom `__new__` singleton — fights the language, resists testing):
class Config:
_instance = None
def __new__(cls):
if cls._instance is None: # also not thread-safe without a lock
cls._instance = super().__new__(cls)
return cls._instance
# Hard to substitute in tests; hides that everyone shares mutable state.Correct (lazy factory via `functools.cache`):
from functools import cache
class Config:
def __init__(self) -> None:
self.theme = "light"
@cache
def get_config() -> Config:
"""First call builds the instance; every later call returns the cached one."""
return Config()
a = get_config()
b = get_config()
a.theme = "dark"
print(a is b, b.theme)Output:
True darkAlternative (module-level instance — the simplest singleton):
# settings.py
class _Settings:
def __init__(self) -> None:
self.theme = "light"
settings = _Settings() # built once when the module is first imported
# elsewhere: from settings import settingsWhen to use
- The program needs exactly one instance of a shared resource for its whole lifetime
- You want lazy initialization — pay the construction cost only on first access (
@cache) - You want one obvious, named access point rather than a free-floating mutable global
When NOT to use
- You reach for it only to avoid passing the object around — that is hidden coupling and hurts tests
- Tests must substitute the instance frequently — prefer dependency injection or
get_config.cache_clear() - The "singleton" is really immutable config — a module-level constant or frozen dataclass is plainer
- State must differ per request/thread/task — a singleton is the wrong scope; use a context var
Implementation Steps
1. Default to a module-level instance when no lazy init is needed 2. For lazy creation, write a factory function and decorate it with functools.cache 3. Import the instance/accessor where needed instead of constructing the class 4. In tests, override the dependency or call factory.cache_clear() to reset 5. Keep the singleton's data immutable where possible to avoid cross-context drift
Pros
- Guarantees one instance with a single, named access point
@cachegives thread-safe lazy initialization for free- Stays testable: the accessor is a seam you can monkeypatch or clear
Cons
- Couples callers to a global and can mask poor design — singletons are easy to overuse
- Mutable singletons across async tasks, threads, or processes diverge unless guarded
- Module-import singletons run their construction at import time, which can surprise
Related Patterns
- Facade — facades are often singletons because one instance suffices
- Flyweight — looks similar but allows many instances (one per intrinsic state) and is immutable
- Abstract Factory / Builder — frequently exposed as a module-level singleton instance
Reference: refactoring.guru/design-patterns/singleton/python
Use Adapter to Make Incompatible Interfaces Cooperate
Pattern intent: let two objects with incompatible interfaces work together by wrapping one in an object that exposes the interface the other expects. In Python, duck typing means you only need an Adapter when the method names or shapes differ — when they already match, pass the object directly. The adapter is a thin wrapper class (or a function) translating one call into another.
Shapes to recognize
- A third-party or legacy class you cannot edit whose method names differ from your code's expectations
- Conversion code (
xml_to_dict(...), reshaping arguments) repeated at every call site - "I want to drop this library in behind my existing interface without rewriting callers"
- Several backends that should be interchangeable but expose different method names
Problem
Application code calls notifier.notify(text). A new requirement routes alerts to Slack, whose client exposes post_message(channel, body) instead. You cannot change the vendor class, and sprinkling client.post_message("ops", text) across the app couples every caller to Slack's shape.
Solution
Write an adapter that implements the interface callers expect (Notifier) and forwards to the adaptee's actual API, translating arguments. Callers keep depending on the Protocol; only the adapter knows the vendor's method names.
Incorrect (every caller translates to the vendor API by hand):
client = SlackClient()
# Each site couples to Slack's signature; swapping vendors means editing all of them.
client.post_message("ops", "disk 90% full")
client.post_message("ops", "deploy finished")Correct (one adapter exposes the expected interface):
from typing import Protocol
class Notifier(Protocol):
def notify(self, text: str) -> None: ...
class SlackClient: # third-party adaptee; cannot be modified
def post_message(self, channel: str, body: str) -> None:
print(f"slack#{channel}: {body}")
class SlackNotifier: # adapter: Notifier interface → SlackClient API
def __init__(self, client: SlackClient, channel: str) -> None:
self._client, self._channel = client, channel
def notify(self, text: str) -> None:
self._client.post_message(self._channel, text)
def send_alerts(notifier: Notifier) -> None: # depends only on the Protocol
notifier.notify("disk 90% full")
send_alerts(SlackNotifier(SlackClient(), channel="ops"))Output:
slack#ops: disk 90% fullWhen to use
- You want to use an existing class whose interface doesn't match what your code expects
- You need several interchangeable backends that happen to expose different method names
- You are isolating a volatile third-party API behind a stable seam
When NOT to use
- The adaptee already has the methods your code calls — duck typing means no adapter is needed
- The translation is one trivial call — a small function or
functools.partialis lighter than a class - You actually need to simplify a whole subsystem, not match one interface — that is Facade
Implementation Steps
1. Define (or identify) the Protocol your callers expect 2. Create an adapter class that accepts the adaptee via its constructor 3. Implement each expected method by translating arguments and delegating to the adaptee 4. Type call sites against the Protocol, not the concrete adapter 5. Add more adapters for additional backends as needed; callers stay unchanged
Pros
- Single Responsibility: interface translation lives in one class, away from business logic
- Open/Closed: new backends arrive as new adapters without touching callers
- Lets incompatible or legacy classes participate behind a clean interface
Cons
- Adds a wrapper class and one layer of indirection
- For deep interface mismatches the adapter can grow complex — sometimes changing the caller is simpler
Related Patterns
- Facade — defines a new simplified interface over a subsystem; Adapter reuses an existing interface
- Decorator — keeps the same interface and adds behavior; Adapter changes the interface
- Proxy — keeps the same interface and controls access; Adapter converts it
- Bridge — designed up front to vary two sides; Adapter retrofits cooperation after the fact
Use Bridge to Split Abstraction from Implementation
Pattern intent: when a class varies along two independent dimensions, split it into an abstraction hierarchy and an implementation hierarchy linked by composition, so each side grows on its own. In Python the implementation side is a Protocol; the abstraction holds a reference to it and delegates.
Shapes to recognize
- Subclass names that multiply two axes:
TVRemote,RadioRemote,AdvancedTVRemote,AdvancedRadioRemote - A
ShapexColor(orUIxPlatform,DocumentxRenderer) matrix headed form * nclasses - Two things changing for unrelated reasons crammed into one inheritance tree
- "Every time I add a device I have to add it to every kind of remote"
Problem
A remote control comes in basic and advanced variants and must drive TVs and radios. Modeling this with inheritance gives a class per combination — adding a streaming box or a third remote type multiplies the count, and shared logic gets copied across siblings.
Solution
Treat "remote" (abstraction) and "device" (implementation) as separate hierarchies. The remote holds a Device and delegates to it; remote variants extend the abstraction, device variants implement the Protocol. Any remote works with any device — m + n classes instead of m * n.
*Incorrect (one class per combination — `m n` explosion):**
class BasicTVRemote: ...
class BasicRadioRemote: ...
class AdvancedTVRemote: ...
class AdvancedRadioRemote: ...
# Add a streaming box, or a "kids" remote, and the matrix grows again.Correct (abstraction composes an implementation Protocol):
from typing import Protocol
class Device(Protocol): # implementation side
def get_volume(self) -> int: ...
def set_volume(self, pct: int) -> None: ...
def name(self) -> str: ...
class TV:
def __init__(self) -> None: self._vol = 0
def get_volume(self) -> int: return self._vol
def set_volume(self, pct: int) -> None: self._vol = max(0, min(100, pct))
def name(self) -> str: return "TV"
class Radio:
def __init__(self) -> None: self._vol = 0
def get_volume(self) -> int: return self._vol
def set_volume(self, pct: int) -> None: self._vol = max(0, min(100, pct))
def name(self) -> str: return "Radio"
class RemoteControl: # abstraction side
def __init__(self, device: Device) -> None:
self._device = device
def volume_up(self) -> None:
self._device.set_volume(self._device.get_volume() + 10)
class AdvancedRemote(RemoteControl): # extends abstraction, not the matrix
def mute(self) -> None:
self._device.set_volume(0)
remote = AdvancedRemote(TV()) # any remote x any device
remote.volume_up()
print(f"{remote._device.name()} @ {remote._device.get_volume()}%")Output:
TV @ 10%When to use
- A class varies along two (or more) independent dimensions
- You want to extend each dimension without touching the other
- You need to switch the implementation at runtime (pass a different
Device)
When NOT to use
- There is only one dimension of variation — plain composition or a strategy is enough
- The two hierarchies are tiny and stable — the indirection costs more than it saves
- You are retrofitting cooperation between existing incompatible classes — that is Adapter
Implementation Steps
1. Identify the two independent dimensions in the bloated class 2. Declare a Protocol for the implementation dimension (the lower-level operations) 3. Give the abstraction a field holding an implementation and delegate primitive operations to it 4. Extend the abstraction with refined variants that combine the primitive operations 5. Implement the Protocol once per concrete implementation; inject it through the constructor
Pros
- Decouples interface from implementation —
m + nclasses instead ofm * n - Each hierarchy evolves and ships independently (Open/Closed)
- Implementations are swappable at runtime through composition
Cons
- Upfront design overhead; over-applied to a class with only one axis it just adds layers
- The indirection can obscure simple cases
Related Patterns
- Strategy — same composition shape for one varying behavior; Bridge separates two whole hierarchies
- Abstract Factory — can create and pair matching abstraction/implementation objects
- Adapter — makes existing classes cooperate after the fact; Bridge is designed in up front
- State — also delegates to a swapped object, but to change behavior as state changes
Use Composite to Treat Trees and Leaves Uniformly
Pattern intent: compose objects into trees and let clients treat individual objects (leaves) and compositions (branches) through the same interface. Each branch implements the operation by recursing over its children. In Python a shared Protocol plus a recursive method (or __iter__) is all it takes.
Shapes to recognize
- A tree: files/folders, UI widgets/containers, org chart, expression AST, nested groups
- Traversal code full of
isinstance(node, Folder)to decide whether to recurse - An operation (size, render, total price) that must apply to both single items and groups
- "I want to call one method on the root and have it walk the whole structure"
Problem
A file system computes total size. A folder contains files and other folders. Code that special-cases each kind — if isinstance(node, File): ... elif isinstance(node, Folder): ... — repeats the branch at every operation and breaks when a new node type appears.
Solution
Define one Protocol with the operation (size). Leaves implement it directly; composites implement it by summing the result over their children. Clients call the operation on any node without knowing whether it's a leaf or a branch.
Incorrect (`isinstance` branching at every traversal):
def total_size(node) -> int:
if isinstance(node, File):
return node.bytes
elif isinstance(node, Folder):
return sum(total_size(child) for child in node.children)
# Add a Symlink node type and every function shaped like this must change.
raise TypeError(node)Correct (leaf and branch share one interface; branch recurses):
from typing import Protocol
class Node(Protocol):
def size(self) -> int: ...
class File:
def __init__(self, name: str, num_bytes: int) -> None:
self.name, self._bytes = name, num_bytes
def size(self) -> int:
return self._bytes # leaf: base case
class Folder:
def __init__(self, name: str, children: list[Node]) -> None:
self.name, self.children = name, children
def size(self) -> int:
return sum(child.size() for child in self.children) # branch: recurse
root = Folder("/", [
File("a.txt", 100),
Folder("sub", [File("b.txt", 200), File("c.txt", 50)]),
])
print(root.size()) # one call walks the whole treeOutput:
350When to use
- Your data forms a part-whole hierarchy (trees of arbitrary depth)
- Clients should treat single objects and groups of objects identically
- Operations should recurse over the structure without callers managing the recursion
When NOT to use
- The structure is flat — a list and a loop are clearer than a tree abstraction
- Leaves and branches need genuinely different interfaces — forcing one interface adds empty methods
- The hierarchy is fixed and small with one operation — a recursive function with
matchmay read better
Implementation Steps
1. Model the domain as a tree of leaves and containers 2. Declare a Protocol with the operations clients call on any node 3. Implement leaves so the operation returns their own value (base case) 4. Implement composites so the operation aggregates results over children (recursive case) 5. Optionally add __iter__ so the tree supports for node in tree traversal
Pros
- Clients work with arbitrarily complex trees through one interface
- New node types slot in without changing traversal code (Open/Closed)
- Recursion lives inside the structure, not scattered across
isinstancechecks
Cons
- A single shared interface can be awkward when leaf and branch behavior diverge sharply
- Type checks for "is this a leaf?" creep back if clients need to add children only to branches
Related Patterns
- Iterator — traverses a Composite without exposing its structure (
__iter__/generators) - Visitor — applies new operations across a Composite without editing the node classes
- Decorator — also recursive wrapping, but adds responsibilities rather than aggregating children
- Builder — frequently assembles Composite trees
Reference: refactoring.guru/design-patterns/composite/python
Use Decorator to Attach Behaviors at Runtime via Wrappers
Pattern intent: attach new responsibilities to an object by wrapping it in another object with the same interface, stackable in any order. Note the name clash: the GoF Decorator pattern wraps an object; Python's @decorator syntax wraps a function. Both share the idea — wrap to add behavior — and this entry covers the object form, with the function form as the idiomatic shortcut.
Shapes to recognize
- Combinations modeled as subclasses:
CompressedStream,EncryptedStream,CompressedEncryptedStream - Cross-cutting behavior (logging, caching, retry, compression) you want to add per-instance
- "I need to layer two or three behaviors, and any subset, chosen at runtime"
- Repeated boilerplate that wraps a call to add the same before/after logic
Problem
A data stream sometimes needs compression, sometimes encryption, sometimes both, in either order. Modeling each combination as a subclass produces a class per subset — and the count doubles with every new behavior.
Solution
Define the base interface as a Protocol. Each decorator implements that interface, holds the wrapped object, and adds behavior before/after delegating. Stack them at runtime; the outermost wrapper is called first.
Incorrect (a subclass per behavior combination):
class FileStream: ...
class CompressedStream(FileStream): ...
class EncryptedStream(FileStream): ...
class CompressedEncryptedStream(FileStream): ... # and EncryptedCompressedStream, and...
# Add "base64" and you double the subclass count again.Correct (small wrappers sharing one interface, stacked at runtime):
from typing import Protocol
class DataSource(Protocol):
def write(self, data: str) -> str: ...
class FileSource:
def write(self, data: str) -> str:
return data # the concrete component
class CompressionDecorator:
def __init__(self, wrappee: DataSource) -> None:
self._wrappee = wrappee
def write(self, data: str) -> str:
return self._wrappee.write(f"zip({data})")
class EncryptionDecorator:
def __init__(self, wrappee: DataSource) -> None:
self._wrappee = wrappee
def write(self, data: str) -> str:
return self._wrappee.write(f"aes({data})")
# Choose the stack at runtime; outermost runs first.
source: DataSource = EncryptionDecorator(CompressionDecorator(FileSource()))
print(source.write("payroll"))Output:
zip(aes(payroll))Alternative (function decorator with `functools.wraps` for behavior on callables):
import functools
def retry(times: int):
def deco(fn):
@functools.wraps(fn) # preserve name/docstring/signature
def inner(*args, **kwargs):
for attempt in range(times):
try:
return fn(*args, **kwargs)
except ConnectionError:
if attempt == times - 1:
raise
return inner
return decoWhen to use
- You want to add or remove responsibilities at runtime, in arbitrary combinations
- Subclassing for every combination would explode the class count
- The added behavior wraps an existing interface rather than changing it
When NOT to use
- Only one behavior will ever be added — a subclass or a parameter is simpler
- The wrappers need to know each other's order in fragile ways — that coupling defeats the pattern
- You are decorating a function, not an object — use Python's
@decoratorsyntax instead
Implementation Steps
1. Declare the component interface as a Protocol 2. Implement the concrete component (the thing being wrapped) 3. For each behavior, write a decorator that stores the wrappee and implements the interface 4. Add behavior before/after the delegated call inside each decorator method 5. Compose the stack at runtime by nesting constructors
Pros
- Extend behavior without subclassing, and combine behaviors freely
- Add or remove responsibilities at runtime (Single Responsibility per wrapper)
- Avoids the combinatorial subclass explosion
Cons
- Hard to remove a specific wrapper from deep in a stack
- Behavior depends on wrapping order, which can be non-obvious
- Many tiny wrapper classes can clutter a codebase
Related Patterns
- Composite — both wrap recursively; Composite aggregates children, Decorator adds one responsibility and passes through
- Adapter — changes an interface; Decorator keeps the interface and enriches it
- Proxy — same interface but controls access/lifecycle rather than adding behavior
- Strategy — changes the inner algorithm; Decorator changes the outer skin
Reference: refactoring.guru/design-patterns/decorator/python
Related skills
FAQ
What does implementation-design-patterns-python do?
implementation-design-patterns-python is a Claude Code skill for python. It helps developers move faster with AI-assisted coding.
When should I use implementation-design-patterns-python?
When you need to helps with python tasks during ai-assisted development, or when implementation-design-patterns-python is a claude code skill for python. it helps developers move faster with ai-assisted coding.
What are the main capabilities?
implementation-design-patterns-python; Python; AI-coding skill.