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
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.
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).
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.
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.
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.
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.
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?
OOP & design patterns