Before you start
Decorator is easy to spot once you see the pattern: any wrapper class that shares the interface it wraps.
Two things to keep separate:
- Decorator pattern (this lesson) — an object-oriented wrapping technique
- Python's
@decoratorsyntax — a function-wrapping shortcut
They share a name and an idea (wrap and add). They're otherwise different tools. This lesson is about the pattern.
When to use Decorator
Signs:
- You want to add behavior AROUND an object without changing its class. (You can't or shouldn't touch it — third party, shared, or intentionally kept small.)
- You want to STACK behaviors — logging + retry + rate limit, in different combinations.
- Different callers want different combinations. No fixed set of "features" — mixed and matched per caller.
- The behaviors are independent — they don't need to know about each other to work.
If all four fire, Decorator is the clean answer. If only one or two fire, look at alternatives first.
Where Decorator shows up in real code
- HTTP middleware. Every web framework (Django, Express, ASP.NET, Rails) has middleware that wraps requests. That's Decorator.
- Notification wrappers (this lesson) — logging, retry, rate limit around a send.
- File I/O wrappers.
BufferedReader(FileReader(...))in Java. Same shape in Python's io module. - Cache wrappers —
CacheReader(DatabaseReader(...)). Read from cache; on miss, fall through to the database and store the result. - Auth checks — wrap a service with an
AuthorizedNotifierthat raises unless the caller has permission.
Anywhere you catch yourself thinking "I want to add X to Y without changing Y" — Decorator is the tool.
The order-matters trap
Decorators stack, and the order changes the meaning:
LoggingNotifier(RetryingNotifier(EmailNotifier()))— logging sees ONE send (retries happen inside, invisible to logging).RetryingNotifier(LoggingNotifier(EmailNotifier()))— retries wrap logging, so a failed send retries the ENTIRE log-plus-send. Log entries get duplicated per retry.
Both are correct — for different intents. Pick the
order on purpose. In this lesson,
LoggingNotifier(RetryingNotifier(...)) is usually what
you want.
The extensibility payoff
Look at the test_deep_stack test. To wire up "log +
retry + rate limit around email", we don't write a new
class. We compose four existing objects. The next
customer wants "just retry around Slack"? Same pattern,
different composition, zero new classes.
That's why Decorator scales so well when you have many independent features. Each is one class. Every combination is free.
Decorator vs the other "wrap-something" patterns
| Pattern | Same interface? | Adds what? |
|---|---|---|
| Adapter | No — the target shape differs from the adaptee | Nothing; translates only |
| Facade | New interface (usually simpler) | Nothing; coordinates only |
| Decorator | Yes — wrapper matches the wrapped interface | New behavior AROUND the call |
| Proxy | Yes | Access control, lazy loading, caching (doesn't change observable behavior) |
Decorator is the "same interface, added behavior" one.
Common mistakes
Decorators that reach into fields the wrapped class owns. If
LoggingNotifierreaches for_inner.some_field, that's a leak. Decorators should only use the wrapped interface.Decorators that CHANGE the return type.
send()returnsstr— decorators should still returnstr. If a decorator returns aLoggedResultobject, it's no longer a drop-in replacement.Decorators that DON'T call the inner method. A "logging" decorator that just logs and never sends is a silent bug. Every decorator should either: (a) call inner exactly once, OR (b) explicitly refuse to (like rate-limit's "rate-limited" case)
Deeply nested constructor calls that are unreadable. Pull the composition into a function:
def build_enterprise_notifier(email): return LoggingNotifier( RetryingNotifier( RateLimitedNotifier(email, 100), max_attempts=3, ) )Now callers see
build_enterprise_notifier(EmailNotifier()).
The decision cheat-sheet
| Situation | Pick |
|---|---|
| Add ONE behavior around an object, forever | Inheritance is fine |
| Add multiple independent behaviors | Decorator |
| Combinations chosen at runtime | Decorator |
| Every combination is fixed at code time | Inheritance or single class with flags |
| Wrap a third-party object | Decorator (composition) |
| Add UNRELATED abilities to different objects | Plain composition (not Decorator specifically) |