
Implementing the Strategy Pattern in Python for Cleaner Code Organization
Discover how the Strategy design pattern helps you organize code, swap algorithms at runtime, and make systems (like chat servers or message routers) more maintainable. This practical guide walks through concepts, step-by-step examples, concurrency considerations, f-string best practices, and advanced tips for production-ready Python code.
Introduction
Have you ever ended up with a big conditional block full of if/elif
branches deciding how to process data? The Strategy pattern gives you a clean, object-oriented way to encapsulate those algorithms and swap them at runtime without touching the client code.
In this post you'll learn:
- What the Strategy pattern is and when to use it.
- How to implement it in Python idiomatically (with
abc
,typing
, and composition). - How to handle concurrency and race conditions when strategies are changed at runtime.
- How to use Python f-strings properly within strategies (including localization considerations).
- A real-world flavored example: applying Strategy to a simple real-time chat message handling system (pairs nicely with WebSockets).
asyncio
and threading
before. Let's dive in.
Prerequisites
Before implementing the Strategy pattern, ensure you're comfortable with:
- Classes and composition.
- Abstract base classes (
abc.ABC
) and thetyping
module. - Basic concurrency concepts (threads, asyncio).
- Python string formatting (f-strings).
- Familiarity with web sockets and chat app architecture is helpful but not required.
- Official docs on classes and
abc
: https://docs.python.org/3/library/abc.html - PEP 498 (f-strings): https://www.python.org/dev/peps/pep-0498/
- asyncio docs: https://docs.python.org/3/library/asyncio.html
- threading docs: https://docs.python.org/3/library/threading.html
Core Concepts: What is the Strategy Pattern?
At its core, the Strategy pattern:
- Defines a family of algorithms.
- Encapsulates each algorithm behind a common interface.
- Makes the algorithms interchangeable within a context object.
- Removes large conditional statements.
- Encourages Single Responsibility Principle.
- Makes swapping and testing algorithms easy.
if
selecting encoding logic, give the player a codec strategy that can be swapped—without changing the player.
High-Level Strategy Structure
Typical components:
- Strategy interface (abstract base class defining the contract).
- Concrete strategies (implementations of the interface).
- Context (holds a reference to a strategy and delegates behavior).
- Client code (creates the context and provides a strategy).
- Client -> Context -> Strategy Interface -> Concrete Strategies
Step-by-Step Examples
We'll build two progressively realistic examples:
- A simple calculation strategy to introduce the concept.
- A chat message handler that uses strategies for routing/encoding, showcasing concurrency, f-strings, and real-time app considerations.
Example 1 — Simple Calculation Strategy
We define a strategy interface for computing a score. Concrete strategies provide different scoring formulas.
from abc import ABC, abstractmethod
from typing import Protocol
class ScoringStrategy(ABC):
@abstractmethod
def compute(self, value: int) -> float:
"""Compute a score based on an integer input."""
pass
class LinearScoring(ScoringStrategy):
def __init__(self, factor: float = 1.0):
self.factor = factor
def compute(self, value: int) -> float:
return value * self.factor
class LogScoring(ScoringStrategy):
import math
def compute(self, value: int) -> float:
# protect against log(0)
return 0.0 if value <= 0 else self.math.log(value)
class Context:
def __init__(self, strategy: ScoringStrategy):
self._strategy = strategy
def set_strategy(self, strategy: ScoringStrategy):
self._strategy = strategy
def score(self, value: int) -> float:
return self._strategy.compute(value)
Usage
c = Context(LinearScoring(2.0))
print(c.score(10)) # 20.0
c.set_strategy(LogScoring())
print(c.score(10)) # ~2.302585092994046
Line-by-line explanation:
- Lines 1–3: Import ABC and typing tools.
- Lines 5–8:
ScoringStrategy
declarescompute()
to ensure all strategies have the same method signature. LinearScoring
multiplies input by a factor.LogScoring
uses math.log; returns 0 for non-positive inputs to avoid exceptions (an explicit edge-case guard).Context
holds_strategy
and delegatesscore()
to it.- Usage shows runtime swapping with
set_strategy()
.
LogScoring
handlesvalue <= 0
.- If a strategy raises an unhandled exception, the context should either catch it or let it bubble up—depending on desired semantics.
Example 2 — Chat Message Handling with Strategies
Imagine a simple chat server. Messages need:
- Routing strategy (broadcast, private, room).
- Encoding strategy (JSON, plain text, compressed).
We'll show a synchronous, clear implementation then discuss async & race conditions.
#### Strategy Interfaces and Concrete Implementations
# strategies.py
from abc import ABC, abstractmethod
from typing import Dict, Any, List
import json
class EncodingStrategy(ABC):
@abstractmethod
def encode(self, payload: Dict[str, Any]) -> bytes:
pass
class JsonEncoding(EncodingStrategy):
def encode(self, payload: Dict[str, Any]) -> bytes:
# Example: Use f-strings for small snippets inside payload for readability.
# But prefer json.dumps for full serialization.
return json.dumps(payload, ensure_ascii=False).encode('utf-8')
class PlainTextEncoding(EncodingStrategy):
def encode(self, payload: Dict[str, Any]) -> bytes:
# Convert to a simple line-based representation
return f"{payload.get('user')}: {payload.get('message')}".encode('utf-8')
class RoutingStrategy(ABC):
@abstractmethod
def recipients(self, server_state: Dict[str, Any], sender: str, target: str) -> List[str]:
pass
class BroadcastRouting(RoutingStrategy):
def recipients(self, server_state, sender, target):
# send to all connected users
return list(server_state.get('connections', {}).keys())
class PrivateRouting(RoutingStrategy):
def recipients(self, server_state, sender, target):
# target is a username
return [target] if target in server_state.get('connections', {}) else []
Explanation:
EncodingStrategy
/RoutingStrategy
define interfaces.JsonEncoding
returnsbytes
usingjson.dumps
andutf-8
. Noteensure_ascii=False
for proper Unicode handling — important for localization.PlainTextEncoding
demonstrates using an f-string for small concatenation. For localized messages, prefer translation functions over f-strings (explained later).BroadcastRouting
andPrivateRouting
compute recipient lists fromserver_state
.
Switching strategies at runtime can produce race conditions if concurrent handlers read or write the strategy reference simultaneously. We show a thread-safe approach for both thread-based and async-based servers.
Synchronous (threading) safe handler:
# handler_threadsafe.py
from threading import Lock
from typing import List, Dict, Any
class MessageHandler:
def __init__(self, routing: RoutingStrategy, encoding: EncodingStrategy):
self._routing = routing
self._encoding = encoding
self._lock = Lock() # protects strategy swaps
def set_routing(self, routing: RoutingStrategy):
with self._lock:
self._routing = routing
def set_encoding(self, encoding: EncodingStrategy):
with self._lock:
self._encoding = encoding
def handle(self, server_state: Dict[str, Any], sender: str, target: str, payload: Dict[str, Any]) -> List[bytes]:
# Copy pointers under lock for stable snapshot
with self._lock:
routing = self._routing
encoding = self._encoding
recipients = routing.recipients(server_state, sender, target)
encoded = encoding.encode(payload)
# emulate sending: return list of (recipient, encoded) bytes
return [(r, encoded) for r in recipients]
Line-by-line:
Lock
ensures that swaps (set_routing
/set_encoding
) and reads inhandle()
don't interleave inconsistently.- In
handle
, we acquire the lock briefly and copy references to local variables — this reduces lock duration (good for performance). - This avoids a race where the encoding changes mid-send causing inconsistent behavior.
- Without the
Lock
, one thread might callhandle()
while another callsset_encoding()
; runtime behavior is unpredictable. - If your application uses
asyncio
, useasyncio.Lock()
and makehandle
anasync def
function.
# handler_async.py
import asyncio
from typing import Dict, Any, List, Tuple
class AsyncMessageHandler:
def __init__(self, routing: RoutingStrategy, encoding: EncodingStrategy):
self._routing = routing
self._encoding = encoding
self._lock = asyncio.Lock()
async def set_routing(self, routing: RoutingStrategy):
async with self._lock:
self._routing = routing
async def set_encoding(self, encoding: EncodingStrategy):
async with self._lock:
self._encoding = encoding
async def handle(self, server_state: Dict[str, Any], sender: str, target: str, payload: Dict[str, Any]) -> List[Tuple[str, bytes]]:
async with self._lock:
routing = self._routing
encoding = self._encoding
recipients = routing.recipients(server_state, sender, target)
encoded = encoding.encode(payload)
return [(r, encoded) for r in recipients]
This pattern addresses Handling Race Conditions in Python: Practical Solutions and Patterns by:
- Minimizing lock scope.
- Using appropriate lock types (
threading.Lock
vsasyncio.Lock
). - Favoring immutable data and references when possible.
Using f-strings in Strategies: Best Practices and Localization
f-strings are concise and performant. Best practices:
- Use f-strings for composing strings where expressions are simple and short.
- Avoid embedding long computations inside f-strings—compute separately for readability.
- Do NOT use f-strings for strings that require localization. For translatable strings, use
gettext
orbabel
and format with.format()
or by usinggettext
placeholders.
from gettext import gettext as _
user = "alice"
message = "Hello"
BAD for localization:
s_bad = f"{user} says: {message}"
Better (translate template, then format):
template = _("{} says: {}")
s_good = template.format(user, message)
Why? Translators may need to reorder placeholders; format()
with positional or named placeholders handles this.
When using f-strings in strategies:
- Use them for debug or simple log messages.
- For the message payload in WebSocket chat apps, prefer structured formats (JSON) for predictable client parsing.
%
or .format()
in most cases (see PEP 498 and benchmarks), but the readability and localization constraints matter more for real-world apps.
Applying Strategy Pattern in a WebSocket Chat App
Where will strategies help in a real-time chat app?
- Pluggable message encoding (JSON, MessagePack, Protobuf).
- Pluggable routing (broadcast, room-based, permission-filtered).
- Pluggable persistence (no storage, Redis-backed, DB-backed).
- Use
aiohttp
orwebsockets
for the server. - Instantiate
AsyncMessageHandler
with chosen strategies. - On incoming message, parse, and call handler.handle().
Best Practices
- Define small, focused strategy interfaces.
- Keep strategies stateless if possible. Stateless strategies are naturally thread-safe.
- If state is necessary, make state immutable or protect with locks.
- Use dependency injection: provide strategies via constructor or setter functions.
- Minimize the lock holding time to prevent performance bottlenecks.
- Write unit tests for each concrete strategy and integration tests for the context.
- Use f-strings for non-localizable strings and structured serialization (JSON) for messages exchanged over the network.
- For production chat apps, consider message size, encoding efficiency, and serialization safety (avoid
pickle
over network).
Common Pitfalls
- Overusing the pattern: If there are only two small branches, using full Strategy classes might be overkill.
- Mutable shared strategy state without synchronization leads to race conditions.
- Using f-strings for translatable strings breaks localization.
- Forgetting to handle unexpected payloads or missing keys in encoding strategies (always validate inputs).
- Mixing blocking I/O with asyncio without offloading to thread pools.
Advanced Tips
- Use function-based strategies for tiny behaviors (callable objects) instead of full classes — Python supports duck typing.
- Compose strategies: you can chain encoding strategies (e.g., compress after JSON encoding).
- Use
typing.Protocol
for lighter-weight interfaces if you prefer structural typing. - Consider using a factory for strategy creation to manage configuration-driven selection.
- For high-throughput systems, measure lock contention and shallow-copy references to local variables to reduce contention.
class CompressedJsonEncoding(EncodingStrategy):
def __init__(self, base: EncodingStrategy, compressor):
self.base = base
self.compressor = compressor
def encode(self, payload):
raw = self.base.encode(payload) # bytes
return self.compressor.compress(raw)
Error Handling and Testing
- Strategies should validate inputs and raise meaningful exceptions.
- Context should decide whether to catch strategy exceptions or allow them to bubble up (fail-fast vs resilient server model).
- Write unit tests for each strategy and integration tests for the handler.
- Use mocks for network I/O.
- Test dynamic swapping of strategies under concurrency (e.g., using
ThreadPoolExecutor
orasyncio
tasks).
Conclusion
The Strategy pattern is a pragmatic, maintainable way to structure code when multiple interchangeable algorithms exist. In Python you can implement it idiomatically using abc
, typing
, and locks (for concurrency). When building real-time systems like chat apps (WebSockets), Strategy shines for routing and encoding responsibilities. Pair it with good concurrency practices to avoid race conditions, and use f-strings mindfully—never for translatable templates.
Try it now:
- Refactor a module in your code that uses long
if/elif
chains into strategies. - Build a small WebSocket demo swapping between JSON and plain-text encoding strategies at runtime.
Further Reading
- The Gang of Four — Design Patterns (Strategy)
- Python docs: abc, threading, asyncio
- PEP 498 — Literal String Interpolation (f-strings)
- Tutorials on WebSockets in Python (aiohttp, websockets)
- Articles on Handling Race Conditions in Python: Practical Solutions and Patterns
- Provide a complete minimal WebSocket chat app example using
websockets
showing strategy swaps at runtime. - Convert the examples into test suites to validate correctness and concurrency safety.
Was this article helpful?
Your feedback helps us improve our content. Thank you!