I remember the exact moment decorators clicked for me. I was staring at @app.route("/home") in a Flask app, wondering how a single line above a function could change so much about how that function behaved. It turns out decorators aren’t magic at all — they’re just functions that take a function and return another function. Once I understood that, an entire category of “advanced Python” stopped feeling advanced and started feeling like a natural extension of things I already knew: functions as first-class objects.
This guide is my complete walkthrough of decorator functions — from the absolute basics to the internal mechanics, memory behavior, and the patterns I reach for in real code.
Functions Are First-Class Objects
Before decorators make sense, you need to internalize one fact: in Python, functions are objects like any other. You can assign them to variables, pass them as arguments, and return them from other functions.
def greet():
return "Hello!"
say_hi = greet
print(say_hi()) # Hello!
Decorators are built entirely on this property, plus the ability to define functions inside other functions (closures).
The Simplest Possible Decorator
def my_decorator(func):
def wrapper():
print("Something before the function runs")
func()
print("Something after the function runs")
return wrapper
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)
say_hello()
Output:
Something before the function runs
Hello!
Something after the function runs
The @ syntax is just shorthand for the manual reassignment on the third-to-last line:
@my_decorator
def say_hello():
print("Hello!")
say_hello()
This produces identical output. I always tell people to mentally expand @decorator into func = decorator(func) whenever they get confused — it demystifies the syntax immediately.
Handling Arguments With *args and **kwargs
My first decorators broke the moment I tried to decorate a function that took arguments, because wrapper() had no parameters. The fix is to make wrapper accept and forward arbitrary arguments:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
add(3, 5)
Output:
Calling add with args=(3, 5), kwargs={}
add returned 8
Why functools.wraps Matters
Without functools.wraps, add.__name__ would be "wrapper", add.__doc__ would be None, and tools relying on introspection — like help(), Sphinx documentation generators, or inspect.signature() — would give misleading results. I’ve been bitten by this in real projects where automated API docs quietly listed every decorated endpoint as wrapper, which was useless for anyone reading the docs.
print(add.__name__) # 'add' with functools.wraps, 'wrapper' without it
Internal Working: Closures and Cell Objects
The key mechanism behind decorators is the closure. When wrapper references func from its enclosing scope, Python doesn’t copy func — it creates a cell object that both my_decorator and wrapper share a reference to. You can inspect this directly:
print(add.__closure__)
print(add.__closure__[0].cell_contents)
This showed me, very concretely, that the “magic” of decorators is just Python’s standard closure mechanism, no different from any other nested function that references an outer variable.
Decorators That Return Different Values
A decorator doesn’t have to call the original function at all — it can conditionally skip it, transform its output, or even replace it entirely. I use this for feature flags:
def disabled(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"{func.__name__} is disabled, skipping.")
return None
return wrapper
@disabled
def experimental_feature():
print("Running experimental logic")
experimental_feature()
Output:
experimental_feature is disabled, skipping.
The original function body never executes — this is a legitimate and common pattern for feature toggles in production systems.
Stacking Multiple Decorators
Decorators apply bottom-up but execute top-down at call time:
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def text():
return "Hello"
print(text())
Output:
<b><i>Hello</i></b>
italic wraps text first, then bold wraps the result. Getting this order backwards produced <i><b>Hello</b></i> in one of my earlier HTML-generation scripts, which taught me to always trace decorator order carefully when the output actually matters.
Performance and Memory Behavior
Each decorated function introduces one additional stack frame per call — the wrapper function. For the overwhelming majority of applications, this cost is irrelevant; Python function calls in general aren’t free, and one more layer typically adds low single-digit microseconds. Memory-wise, each decoration creates a new function object (wrapper) plus a closure cell holding a reference to the original function — this is small and fixed, not proportional to how many times the function is called.
Where it does matter: if you’re decorating something called millions of times in a numerical hot loop, or if you stack five or six decorators, the cumulative call overhead becomes measurable. I’ve profiled this with cProfile on data pipelines and seen decorator overhead show up as a nontrivial chunk of total time only when decorators were doing real work per call (like JSON serialization for logging) — the wrapping itself was cheap.
Real-World Use Cases
- Logging: wrapping functions to record calls, arguments, and return values.
- Timing/profiling: measuring execution duration with
time.perf_counter(). - Authentication/authorization: gating web framework view functions.
- Caching:
functools.lru_cacheis itself a built-in decorator function. - Input validation: checking types or ranges before the real function runs.
- Retry logic and circuit breakers for unreliable network calls.
Here’s a timing decorator I use often:
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
return wrapper
@timer
def compute():
return sum(i * i for i in range(1_000_000))
compute()
Best Practices and Common Mistakes
- Always accept
*args, **kwargsin the wrapper unless you’re certain the decorated function’s signature is fixed. - Always apply
functools.wraps(func)to preserve metadata. - Keep decorators focused on one concern — a decorator that logs, caches, and validates input is harder to test and reuse than three small decorators.
- Be careful decorating class methods without accounting for
selfas the first positional argument —*argshandles this naturally, but explicit signatures don’t. - Avoid decorators with hidden side effects at import time; the decorator function itself runs once, at definition time, not at call time — a mistake I made when I put expensive setup code directly in the decorator body instead of inside
wrapper.
Troubleshooting Tips
If a decorated function raises TypeError: wrapper() takes 0 positional arguments but 1 was given, your wrapper signature doesn’t forward arguments — add *args, **kwargs.
If inspect.signature(func) shows the wrong signature, functools.wraps alone doesn’t fully fix this in older Python versions for complex signatures — consider functools.wraps combined with proper testing, since wraps does copy __wrapped__, which inspect.signature can follow starting from Python 3.4+.
If exceptions inside the decorated function seem to get swallowed, check whether the decorator has a bare try/except block that isn’t re-raising.
FAQs
What’s the difference between a decorator and a decorator function? They’re the same thing when the decorator itself is implemented as a function (as opposed to a class implementing __call__).
Can a decorator function decorate a class? Yes — a decorator applied to a class receives the class object itself and can modify or wrap it, though class decorators are used less often than function decorators.
Do decorators run every time the function is called? No — the decorator function itself runs once, at definition time. The wrapper it returns is what runs on every call.
Is @functools.lru_cache a good example of a decorator function? Yes, it’s one of the most widely used built-in decorator functions in the standard library, and a great reference implementation to study.
Summary
A decorator function is simply a function that accepts a function and returns a new function, typically one that wraps the original with extra behavior before or after execution. The @ syntax is pure syntactic sugar for reassignment. Understanding closures, *args/**kwargs forwarding, and functools.wraps covers the vast majority of real-world decorator use — from logging and timing to caching and authorization — and once these pieces click, decorators stop feeling like a special language feature and start feeling like ordinary function composition.
