Design Patterns For Practical Python Development
Design patterns are typical solutions to problems that commonly occur in software design and architecture. They represent best practices that have evolved over time to address recurring design challenges.
In my previous articles on Abstract Classes and Composition vs Inheritance, we explored how to structure relationships between objects. If you’ve followed along, you already have most of what you need to recognize design patterns. You’ve been writing them. Perhaps you just didn’t have names for them. They’re the shapes that good code naturally settles into when you let it.
Why Use Design Patterns?
Good design isn’t about writing clever code — it’s about writing code that survives change. Learning design patterns helps you:
- Reduce duplication
- Increase flexibility
- Avoid tight coupling
- Make intent obvious
Design patterns are tested, well-documented, and understood by experienced developers. Using them makes code more standardized, portable, and easier to understand, as they provide a common language of sorts.
We’re going to look at three essential design patterns every Python developer should have in their toolbox:
- Template Method (a bridge from abstract classes to real-world structure)
- Strategy Pattern (composition in action)
- Factory Pattern (clean object creation)
Template Method Pattern
The idea behind the template method pattern is to define the skeleton of an algorithm and use subclasses to fill in specific steps. The base class is in charge. The subclasses customize. This pattern is a natural fit for Python’s abstract base class (abc), which we covered previously.
When to Use It
- You have a process with strictly defined steps
- Some steps vary between implementations
- You want to enforce structure but allow flexibility
Example: A Data Processing Pipeline
from abc import ABC, abstractmethod
class DataProcessor(ABC):
def process(self, data):
self.load(data)
self.transform()
self.save()
@abstractmethod
def load(self, data):
pass
@abstractmethod
def transform(self):
pass
def save(self):
print("Saving processed data...")Concrete Implementation
class JSONProcessor(DataProcessor):
def load(self, data):
print("Loading JSON data")
def transform(self):
print("Transforming JSON data")JSONProcessor().process() runs the full pipeline. The order — load, transform, save — is locked in by the DataProcessor base class. How each step is implemented is up to the subclass. This is useful when you have a process that’s the same shape every time but different in the details. ETL jobs. Test runners. Game loops. Web request handlers. The base class enforces the order so nobody can accidentally skip the transform step or run save before load. You probably have seen this pattern before. Django’s class-based views are Template Method. unittest.TestCase with its setUp / test_* / tearDown lifecycle is Template Method. Most ORM model save hooks are Template Method.
⚠️ Common Mistake
Don’t overuse inheritance.
Template Method’s failure mode is the same as inheritance’s: the base class does too much. You start with a clean four-step pipeline, and three years later it’s a fourteen-step monster with optional hooks, conditional steps, and a flag called legacy_mode preceded by comments telling developers “DO NOT TOUCH!”.
If behavior needs to change at runtime, this pattern isn’t the best fit.
Get Marcus T’s stories in your inbox
Join Medium for free to get updates from this writer.
👉 That’s where Strategy comes in.
Strategy Pattern
The idea behind the strategy pattern is to encapsulate interchangeable behaviors and make them pluggable at runtime. It is the pattern that falls out naturally when you take “favor composition over inheritance” seriously.
When to Use It
- You have multiple ways to perform a task
- You want to switch behavior dynamically
- You want to avoid large
if/elifblocks
Instead of locking behavior into a class via inheritance, you inject the behavior as an object. The class uses a thing instead of being a thing. And because the thing is just an object, it becomes dynamic. You can swap it at runtime, mock it in tests, or pick it from a config file.
Example: Computing Shipping Costs
Suppose you have an order class that needs to compute shipping costs based on region.
class StandardShipping:
def cost(self, order):
return 5.00 + 0.50 * order.weight
class ExpressShipping:
def cost(self, order):
return 15.00 + 1.20 * order.weight
class FreeShipping:
def cost(self, order):
return 0.0class Order:
def __init__(self, items, shipping_strategy):
self.items = items
self.weight = sum(item.weight for item in items)
self.shipping_strategy = shipping_strategy
def total(self):
subtotal = sum(item.price for item in self.items)
return subtotal + self.shipping_strategy.cost(self)Order doesn't know or care which shipping rule applies. You inject the strategy:
order = Order(items, ExpressShipping())This makes it easy to swap in new shipping methods for A/B testing or fixed values for unit tests.
⚠️ Things To Avoid
1) Over-application. If a method only has one variant and there’s no realistic chance of a second, Strategy is overkill. A plain method on the class is fine. Don’t increase complexity for flexibility you’ll never use.
2) Strategies need to share an interface. If StandardShipping.cost(order) returns a float but ExpressShipping.calculate(order) returns a dict, you don’t have strategies — you have three unrelated classes that happen to live in the same folder. This is exactly where an ABC or a typing.Protocol comes into play: it pins down the contract so all your strategies actually fit the same slot.
Factory Pattern
The idea behind the Factory pattern is to encapsulate object creation so your code isn’t riddled with if/else logic. You have the Template pattern for the skeleton. You have the Strategy pattern for the swappable behavior. But which one do we instantiate? The Factory pattern uses a function or class that uses configuration input to return the appropriate concrete class for the situation.
When to Use It
- Object creation is complex
- You need to choose between multiple types
- You want to decouple instantiation from usage
Let’s implement a factory for the shipping strategy defined earlier:
def shipping_for(user, region):
if user.is_prime:
return FreeShipping()
if region == "domestic":
return StandardShipping()
if region == "international":
return ExpressShipping()
raise ValueError(f"No shipping strategy for region {region}")Now the shipping strategy can be defined like so:
strategy = shipping_for(user, region)
order = Order(items, strategy)The Order class still knows nothing about the rules. The calling code still knows nothing about the rules. The factory is the one place where the messy decision lives, and that's the primary benefit — the decision logic is in one place, not fifteen.
⚠️ Common Mistake
Don’t over-engineer factories for simple cases.
If creation is trivial, keep it simple.
Factories become less useful when they do too much. The shipping factory above is fine with three branches. At thirty branches, with nested conditions and string matching on user attributes, it’s a maintenance disaster disguised as a design pattern.
How These Patterns Work Together
These patterns solve different problems and they compose cleanly.
- Template Method → controls structure (inheritance)
- Strategy → controls behavior (composition)
- Factory → controls creation (decoupling)
They solve different problems — and often work best together. A realistic system uses all three. A ReportGenerator (Template Method) might delegate its formatting step to a Formatter strategy (CSV, JSON, PDF). The choice of which formatter to use comes from a formatter_for(report_type) factory. Each pattern stays in its lane; the system remains understandable.
That layering is the real lesson. Patterns aren’t standalone tricks — they’re a vocabulary for the structural decisions you’re making anyway. Knowing the names lets you make those decisions on purpose and communicate them better to others working on the same code.








