The Decorator pattern — wrapping behavior at runtime
Not the same thing as a Python @decorator, though they share a name and a spirit — this is the OOP structural pattern for adding behavior to individual objects without touching their class or affecting other instances.
3 min read
First, the naming collision this lesson has to address
Python's @decorator syntax and the OOP Decorator pattern are related in spirit — both "wrap something to add behavior without changing its source" — but they're not the same mechanism. A Python decorator wraps a function, applied at definition time, affecting every call to that function. The Decorator pattern wraps an object instance, applied at runtime, affecting only that specific instance — two other instances of the same class, undecorated, behave completely normally.
The problem: behavior combinations that don't fit inheritance
class Coffee:
def cost(self):
return 2.00
def description(self):
return "Coffee"
class CoffeeWithMilk(Coffee): ...
class CoffeeWithSugar(Coffee): ...
class CoffeeWithMilkAndSugar(Coffee): ...
class CoffeeWithMilkAndSugarAndWhip(Coffee): ...Every combination of optional add-ons needs its own subclass — this explodes combinatorially, and it can't handle a customer choosing extras at runtime (a subclass hierarchy is fixed at compile time; you can't dynamically compose CoffeeWithMilk and CoffeeWithCaramel into a new class while the program is running).
The fix: wrap the object, one layer per add-on
class Beverage(ABC):
@abstractmethod
def cost(self): ...
@abstractmethod
def description(self): ...
class Coffee(Beverage):
def cost(self):
return 2.00
def description(self):
return "Coffee"
class MilkDecorator(Beverage):
def __init__(self, beverage):
self._beverage = beverage # wraps another Beverage
def cost(self):
return self._beverage.cost() + 0.50
def description(self):
return self._beverage.description() + " + Milk"
class SugarDecorator(Beverage):
def __init__(self, beverage):
self._beverage = beverage
def cost(self):
return self._beverage.cost() + 0.25
def description(self):
return self._beverage.description() + " + Sugar"
order = SugarDecorator(MilkDecorator(Coffee()))
order.cost() # 2.75
order.description() # "Coffee + Milk + Sugar"Each decorator implements the same interface (Beverage) as the thing it wraps, holds a reference to the wrapped object, and calls through to it — adding its own contribution before or after. Because every decorator satisfies the same interface as a plain Coffee, decorators can be stacked in any combination, chosen entirely at runtime, without a single new subclass — SugarDecorator(MilkDecorator(Coffee())) and MilkDecorator(SugarDecorator(Coffee())) are both valid, and both are just Coffee instances as far as any code calling .cost() is concerned.
Why this is genuinely different from subclassing
A subclass changes behavior for every instance of that subclass, decided at class-definition time. A decorator changes behavior for one specific wrapped instance, decided at the moment it's constructed — two Coffee objects can be wrapped completely differently, side by side, in the same running program. This is the concrete version of "favor composition over inheritance": instead of a rigid hierarchy baked in ahead of time, behavior is assembled out of independently swappable pieces at runtime, the same underlying idea the composition-over-inheritance lesson covers more generally.
Where this shows up in code you already use
Python's own built-in I/O stack is a real-world Decorator chain: wrapping a raw byte stream with a buffering layer, then a text-encoding layer, is exactly this pattern — each layer implements a compatible read/write interface, wraps the layer beneath it, and adds one specific capability. Django middleware (covered in its own lesson) is architecturally similar in spirit too: each middleware wraps "the rest of the chain," adding behavior before and after calling through to what it wraps — the same "wrap and delegate, adding your own contribution" shape, applied to request handling instead of objects.
The concrete signal a Decorator belongs somewhere
An exploding number of subclasses covering every combination of optional features (WithMilk, WithSugar, WithMilkAndSugar, ...) is the tell — especially when which combination applies isn't known until runtime. If the combinations are fixed and small, plain subclassing is often simpler and doesn't need the extra indirection; Decorator earns its keep once the combinations are numerous, dynamic, or chosen by the end user.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. How is the OOP Decorator pattern different from Python's @decorator syntax, despite sharing a name?
2. Why does a fixed subclass hierarchy (CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar, ...) fail to handle runtime customization?
3. In `SugarDecorator(MilkDecorator(Coffee()))`, what makes this valid and stackable in any order?
4. What's the concrete signal that the Decorator pattern belongs somewhere?