Customers want milk, sugar, whipped cream, caramel, in any combination, sometimes twice. A subclass for every combination explodes; a pile of boolean flags breaks every time a topping is added. Decorator wraps an object in another object with the same interface that adds one behavior, and lets you stack as many wrappers as you need at runtime.
Intent#
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. (GoF)
Analogy: dressing in layers. Start with a T-shirt; add a sweater if it's cold, a raincoat if it's wet. Each layer adds something, you can stack or remove them, and you underneath stay the same.
The problem: subclass explosion or boolean soup#
The two obvious approaches both break down as add-ons grow:
# Wrong way 1: a subclass per combination
class Coffee:
def get_cost(self) -> float: return 5.0
class CoffeeWithMilk(Coffee):
def get_cost(self) -> float: return 5.5
class CoffeeWithMilkAndSugar(Coffee):
def get_cost(self) -> float: return 5.75
# n add-ons → 2^n classes (10 add-ons = 1,024).
# And "double milk" is impossible.
# Wrong way 2: flags in the base class
class BadCoffee:
def __init__(self, has_milk=False, has_sugar=False, has_whip=False):
self.has_milk = has_milk
self.has_sugar = has_sugar
self.has_whip = has_whip
def get_cost(self) -> float:
cost = 5.0
if self.has_milk: cost += 0.50
if self.has_sugar: cost += 0.25
if self.has_whip: cost += 0.75
return cost
# Adding oat milk means editing this class (violates Open/Closed),
# and booleans can't express quantities like "two sugars".
Structure#
The trick is inheritance plus composition. A decorator:
- is a component (same interface), so it can stand in wherever the original is used, and
- has a component (the object it wraps), to which it delegates before or after adding its own behavior.
| Participant | Role |
|---|---|
| Component | The shared interface (Coffee, Beverage) |
| Concrete component | The core object (BasicCoffee, Espresso) |
| Base decorator | Implements Component and holds a Component; delegates by default |
| Concrete decorators | Override methods to add one feature (Milk, Sugar) |
In the coffee example below, the class hierarchy looks like this:
Coffee(interface)BasicCoffee(concrete component)CoffeeDecorator(abstract: implementsCoffee, wraps aCoffee)MilkDecoratorSugarDecoratorWhippedCreamDecorator
Example 1: coffee shop#
Each decorator adds its price and name on top of whatever it wraps. The base decorator delegates by default, so each concrete decorator only overrides what it changes:
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self) -> float: ...
@abstractmethod
def description(self) -> str: ...
class BasicCoffee(Coffee):
def cost(self) -> float:
return 2.0
def description(self) -> str:
return "Basic Coffee"
class CoffeeDecorator(Coffee):
"""Base decorator: IS a Coffee and HAS a Coffee."""
def __init__(self, coffee: Coffee):
self._coffee = coffee
# Delegate by default
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.50
def description(self) -> str:
return f"{self._coffee.description()}, Milk"
class SugarDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.20
def description(self) -> str:
return f"{self._coffee.description()}, Sugar"
class WhippedCreamDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.70
def description(self) -> str:
return f"{self._coffee.description()}, Whipped Cream"
if __name__ == "__main__":
order: Coffee = BasicCoffee()
order = MilkDecorator(order)
order = SugarDecorator(order)
order = WhippedCreamDecorator(order)
print(order.description())
# Basic Coffee, Milk, Sugar, Whipped Cream
print(f"Cost: ${order.cost():.2f}")
# Cost: $3.40
Trace order.cost(). The call travels inward through every layer, then each layer adds its price on the way back out:
| Step | Call | Returns |
|---|---|---|
| 1 | WhippedCream.cost() calls Sugar.cost() |
waits |
| 2 | Sugar.cost() calls Milk.cost() |
waits |
| 3 | Milk.cost() calls BasicCoffee.cost() |
waits |
| 4 | BasicCoffee.cost() |
2.00 |
| 5 | Milk adds 0.50 |
2.50 |
| 6 | Sugar adds 0.20 |
2.70 |
| 7 | WhippedCream adds 0.70 |
3.40 |
Four component/decorator classes cover every combination.
Example 2: beverages with repeats#
Because add-ons are wrappers, the same one can be applied twice:
from abc import ABC, abstractmethod
class Beverage(ABC):
@abstractmethod
def get_cost(self) -> float: ...
@abstractmethod
def get_description(self) -> str: ...
class Espresso(Beverage):
def get_cost(self) -> float:
return 4.00
def get_description(self) -> str:
return "Espresso"
class BeverageDecorator(Beverage, ABC):
"""Inherits Beverage AND holds a Beverage."""
def __init__(self, beverage: Beverage):
self._beverage = beverage
class Milk(BeverageDecorator):
def get_cost(self) -> float:
return self._beverage.get_cost() + 0.50 # delegate, then add
def get_description(self) -> str:
return f"{self._beverage.get_description()}, Milk"
class Sugar(BeverageDecorator):
def get_cost(self) -> float:
return self._beverage.get_cost() + 0.25
def get_description(self) -> str:
return f"{self._beverage.get_description()}, Sugar"
# Double sugar is just two wrappers
order: Beverage = Sugar(Sugar(Milk(Espresso())))
print(order.get_description()) # Espresso, Milk, Sugar, Sugar
print(f"Total: ${order.get_cost():.2f}") # Total: $5.00
Adding caramel tomorrow is one new class, Caramel(BeverageDecorator). No existing code changes, and toppings are assembled at runtime from what the user picks.
Real-world examples#
Wrapping to add behavior is everywhere once you look:
-
Python's
iostack is the textbook case. A text file fromopen()is layers of wrappers:import io with open("file.txt", "w") as f: f.write("hello") raw = io.FileIO("file.txt") # raw bytes from the OS buffered = io.BufferedReader(raw) # adds buffering text = io.TextIOWrapper(buffered) # adds decoding to str print(text.read()) # hello text.close() -
gzip.GzipFile(fileobj=f)wraps any file object to add compression. -
WSGI/ASGI middleware wraps a web app to add logging, auth or compression.
-
Python's
@decoratorsyntax applies the same idea to functions (wrap and extend), though it's a language feature rather than the GoF class structure. -
Business: insurance add-ons, hotel room extras, delivery options (express, gift wrap, insurance).
When to use it#
Decorator fits when:
- You need to add responsibilities at runtime, not when you write the class.
- Features are optional and combinable (and may repeat).
- Subclassing would explode into combinations.
- You can't modify the original class (third-party code), but you can wrap it.
- You want each feature in its own small class (Single Responsibility).
When not to use it#
Skip it in these cases:
-
There are only one or two variations. A subclass is simpler:
# One kind of promotion doesn't need a decorator class Manager(Employee): ... -
Code relies on object identity or
isinstancechecks against the concrete class. -
The decorator would need internal access to the wrapped object's private state.
Pitfalls#
Watch for these traps:
- Order matters. Wrapping order decides the order in which the layers run. With streams you want to compress before encrypting (encrypted data barely compresses), so stack the wrappers so compression runs first.
- Identity. A decorated object is not the original:
isinstance(order, BasicCoffee)is false, and==may behave unexpectedly. - Many tiny objects. Deep stacks are hard to read, debug and configure; stack traces show every layer. A builder or factory can hide the assembly.
Decorator vs similar patterns#
Several patterns wrap an object; they differ in interface and purpose:
| Pattern | Interface | Purpose |
|---|---|---|
| Decorator | Same | Add behavior, stackable |
| Adapter | Changes | Make an existing class fit |
| Proxy | Same | Control access (lazy loading, permissions, caching) |
| Strategy | Same | Replace an algorithm, not layer on top of it |
| Composite | Same | Manage many children in a tree; Decorator wraps one |
Compared with inheritance: decorators are composed at runtime (Milk(Sugar(coffee))); subclasses are fixed when you write the class.
Key takeaways#
- A decorator is a component and has a component, which is what makes wrappers stackable and interchangeable.
- Calls travel inward through every layer, and each layer adds its part.
- n optional features need n decorator classes, not 2^n subclasses, and repeats come for free.
- Wrapping order is behavior: think about which layer runs first.
- Python's layered
iofile objects are the classic real-world decorator.