Design Patterns in Python
Lesson 5 of 5
5Design Patterns in Python
The big pictureFactory, Observer, and Decorator, three classic solutions to three recurring design problems, with complete Python examples and UML diagrams illustrating each pattern's structure.
What the picture shows: The shop announces "new order" once, and the news reaches email, SMS and the log together. You add a fourth subscriber without touching the shop code.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Design patterns are proven solutions to recurring problems, like well-known recipes. A factory is like a restaurant where you order "a vegetarian meal" and it picks the right cook. An observer is like a newsletter subscription: every subscriber hears the news when it happens. A decorator is like gift wrap added without changing the gift.
Factory is a creational pattern hiding class choice behind a function, Observer a behavioural one notifying subscribers of a change, and Decorator a structural one wrapping an object or function to add behaviour (as Python's @decorator does). Two principles govern them: program to the interface, not the implementation, and prefer composition over inheritance.
Required Fundamentals
This part builds directly on what you learned in Part Nine, every design pattern here you'll essentially draw as a Class Diagram. You also need:
| Concept | Why it matters here |
|---|---|
Abstract Base Classes (abc.ABC) | Used to define the shared "contract" that different classes commit to in each pattern. |
| Duck Typing / Protocol | Python doesn't enforce strict interfaces like Java; typing.Protocol gives you extra flexibility when applying the patterns. |
| Composition from Part Nine | Decorator specifically is a direct application of the composition/association relationship you studied in UML. |
Core Concepts
A Design Pattern isn't a ready-to-copy code template, but a description of a responsibility structure that's proven successful at solving a recurring design problem. It answers three questions: what's the problem? who are the participating parties and their roles? and why does this specific composition make future change easier and safer?
1Creational Patterns
Concerned with how objects are created. Useful when object creation is direct and scattered across many places in the code. Our example: Factory.
2Structural Patterns
Concerned with how objects are composed together. Useful when we want to extend behavior through composition instead of generating a huge number of subclasses. Our example: Decorator.
3Behavioral Patterns
Concerned with how objects communicate and share responsibilities. Useful for decoupling an event's source from the reactions to it. Our example: Observer.
4The Two Governing Principles in the Project
The Three Patterns in Detail
1️⃣ Factory, Centralizing Creation
The problem: object creation is scattered across the code with repeated if/elif statements. Every new type means opening the same old function and editing it, a direct violation of the Open/Closed Principle.
## ❌ The fragile way: every new type means editing this function
def create_shape(kind):
if kind == "circle":
return Circle()
elif kind == "square":
return Square()
# adding "triangle" means coming back to edit this code...
Diagram 13, a Registry-based Factory pattern: the create() function never changes; new types are registered from outside.
## ✅ The solution: an external registry that never touches the core creation logic
class ShapeFactory:
def __init__(self):
self._registry = {}
def register_kind(self, name, cls):
self._registry[name] = cls
def create(self, kind):
return self._registry[kind]() # no if/elif branch, ever
factory = ShapeFactory()
factory.register_kind("circle", Circle)
factory.register_kind("square", Square)
# adding a new shape later never touches create():
factory.register_kind("triangle", Triangle)
2️⃣ Observer, Decoupling the Source from Reactions
The problem: one object (a publisher) needs to inform several other objects (observers) when its state changes, without "knowing" their details or hard-coding their names directly inside it.
Diagram 14, the weather station (Subject) doesn't "know" the phone display or website specifically; it only knows they're "observers" that have update().
class WeatherStation:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def set_temperature(self, value):
self._temperature = value
for obs in self._observers: # doesn't know their real types
obs.update(self._temperature)
class PhoneDisplay:
def update(self, temperature):
print(f"📱 Current weather: {temperature}°")
station = WeatherStation()
station.attach(PhoneDisplay())
station.set_temperature(31)
Diagram 15, the same pattern from a Sequence Diagram perspective: notice the notify loop that calls each observer without distinguishing between them.
3️⃣ Decorator, Composing Behavior Instead of Stacking Inheritance
The problem: a base object has optional, combinable features (like pizza order add-ons). Inheritance needs a subclass for every possible combination, the number of classes explodes with every new feature.
Diagram 16, every add-on (Topping) "wraps" another pizza and adds only its own cost, without touching existing classes.
class Pizza:
def cost(self): return 8.0
def description(self): return "Plain pizza"
class ToppingDecorator(Pizza):
def __init__(self, wrapped: Pizza):
self._wrapped = wrapped
class CheeseTopping(ToppingDecorator):
def cost(self): return self._wrapped.cost() + 1.5
def description(self): return self._wrapped.description() + " + cheese"
class OliveTopping(ToppingDecorator):
def cost(self): return self._wrapped.cost() + 1.0
def description(self): return self._wrapped.description() + " + olives"
order = OliveTopping(CheeseTopping(Pizza()))
print(order.description(), "->", order.cost())
# output: Plain pizza + cheese + olives -> 10.5
Pizza or CheeseTopping at all.Worked example
Myth vs. Reality
| The myth | The reality |
|---|---|
| You must use design patterns in every project | Patterns are tools, not obligations, using them without real design pressure adds complexity with no benefit. |
| Memorizing a pattern's name means understanding it | Knowing this is "Observer" isn't enough; you need to explain why this composition solves your specific problem. |
| GoF patterns must be applied in Python exactly like in Java | Python's dynamism (first-class objects, Duck Typing) often simplifies applying the same idea with less formal code. |
| More abstraction is always better | Abstraction must "earn" its complexity by solving a real maintenance problem, otherwise it's unnecessary complexity. |
Where Are These Patterns Actually Used?
Payment gateways
Factory to create the right payment processor (card, digital wallet, bank transfer) without stacking conditions everywhere payment is created.
Notification systems
Observer decouples "something happened" (a new order, a message) from "notification methods" (email, text message, app notification) which may change or grow over time.
Web request processing (Middleware)
Middleware chains in web frameworks (like Flask/Django) are practically chained Decorators wrapping request processing with layers of permissions, logging, and compression.
User interfaces (UI)
Decoratable UI components (borders, shadows, scrolling) are often built with the Decorator pattern instead of dozens of subclasses.
Try it yourself
You want to add a new payment method without editing the code that creates the old ones. Which pattern?
Factory keeps creation in one place, so you register the new method there instead of hunting for every place a payment is created.
Quick Review Flashcards
Check Your Understanding
1. Why do we say a Registry-based Factory is "closed for modification"?
Because the create() function itself needs no change when adding a new type; we just call register_kind() from outside. The stable code (create) stays as it is, and extension comes through addition, not modification.
2. What's the fundamental difference between Observer and Dependency (from Part Nine)?
Dependency is a general UML relationship describing any temporary reliance between two classes. Observer is a specific design pattern that uses this idea but adds a complete structure: a shared subscriber list, a unified update() method, and a dynamic subscribe/unsubscribe mechanism.
3. When does Decorator become a better choice than direct inheritance?
When the additional features are optional, independent, and combinable in multiple ways, inheritance needs a class for every combination, while Decorator composes any combination dynamically at runtime without extra classes.
4. If you wanted to draw the Observer pattern as a Class Diagram from Part Nine, which UML relationship describes the link between Subject and Observer?
Usually Aggregation (o--): the Subject "holds a list" of Observers but doesn't own their lifecycle, an observer can exist and end independently of the publisher (by subscribing and unsubscribing).
Resources for this part
Sorted by kind: start with the official docs when you want the precise reference, or with an article or video when you want the explanation first.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Articles3
Done with this lesson?
Sign in to save your progress.