Python Decorators
A decorator is a function that takes another function (or class) and returns a new function that usually wraps the original with extra behavior — logging, timing, caching, access control, and more — without changing the original function’s source code. Decorators are one of Python’s most distinctive features, built entirely from ordinary functions, closures, and the @ syntax sugar. Once you understand that a decorator is “just a function call in disguise,” the rest of Python’s more advanced tooling (Flask routes, @property, @staticmethod, functools.lru_cache) becomes far less mysterious.
Overview / How it works
In Python, functions are first-class objects: they can be assigned to variables, stored in data structures, passed as arguments, and returned from other functions. A decorator exploits this by being a higher-order function — a function that accepts a function as input and returns a function as output. The core pattern looks like this:
def decorator(func):
def wrapper(*args, **kwargs):
# code that runs before func
result = func(*args, **kwargs)
# code that runs after func
return result
return wrapper
When you write:
@decorator
def some_function():
...
Python does not do anything magical. It is exactly equivalent to writing:
def some_function():
...
some_function = decorator(some_function)
The name some_function now points to wrapper, not to the original function object. Every time you call some_function(), you are really calling wrapper(), which runs your extra logic and then calls the original function (which is captured by the closure over func). This is why decorators can run code before and after a call, inspect or modify arguments, suppress exceptions, cache return values, or even refuse to call the original function at all.
Because a decorator’s job is to accept one function and return one function, decorators compose naturally: you can stack several on top of one function, and you can build decorators that themselves take configuration arguments (a “decorator factory”). Understanding the object model — that @decorator is sugar for reassignment — is the key to reasoning about anything decorator-related, including tricky ordering questions.
Syntax
@decorator_name
def function_name(params):
...
| Part | Meaning |
|---|---|
@decorator_name |
Applies decorator_name to the function defined immediately below it |
decorator_name |
Any callable that accepts one function argument and returns a callable |
wrapper(*args, **kwargs) |
The inner function that actually replaces the original; must accept whatever arguments the original function accepts |
functools.wraps(func) |
A decorator (from the standard library) applied to wrapper that copies over __name__, __doc__, and other metadata from func |
A decorator that itself needs configuration (for example, @repeat(times=3)) is written as a function that returns a decorator, which in turn returns a wrapper — three nested levels of functions in total.
Examples
Example 1: A basic logging decorator
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 4)
Output:
Calling add((3, 4), {})
add returned 7
add is really wrapper now. When add(3, 4) runs, wrapper receives args=(3, 4) and kwargs={}, prints them, calls the real add, prints the result, and returns it. The caller never notices the extra machinery.
Example 2: A parameterized decorator (decorator factory)
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(times=3)
def greet(name):
return f"Hello, {name}!"
print(greet("Ada"))
Output:
['Hello, Ada!', 'Hello, Ada!', 'Hello, Ada!']
repeat(times=3) is called first and returns decorator. That returned function is then applied to greet, exactly as in a plain decorator. The extra outer layer is only there to capture the configuration value (times) in a closure so wrapper can use it later.
Example 3: Stacking decorators
import functools
def count_calls(func):
calls = 0
@functools.wraps(func)
def wrapper(*args, **kwargs):
nonlocal calls
calls += 1
print(f"Call #{calls} to {func.__name__}")
return func(*args, **kwargs)
return wrapper
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(n):
if n not in cache:
cache[n] = func(n)
return cache[n]
return wrapper
@count_calls
@memoize
def square(n):
return n * n
print(square(4))
print(square(4))
print(square(5))
Output:
Call #1 to square
16
Call #2 to square
16
Call #3 to square
25
Decorators stack bottom-up: the one closest to the function is applied first. Here square is wrapped by memoize first, then that result is wrapped by count_calls. So every call goes through count_calls‘s wrapper first (which counts and prints), then into memoize‘s wrapper (which checks the cache). Because functools.wraps propagated the original name through both layers, func.__name__ still reads "square" even two layers deep.
Under the hood, step by step
To see explicitly that @ is just reassignment, compare a decorated function to its manual equivalent:
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet():
return "hello"
def greet_plain():
return "hello"
greet_plain = shout(greet_plain)
print(greet())
print(greet_plain())
Output:
HELLO
HELLO
Both calls produce identical results because they are doing the identical thing: the name on the left of = (or the name below @shout) is bound to whatever shout() returns, which is wrapper, not the original function. The original greet/greet_plain function objects still exist — they are just captured inside wrapper‘s closure as func, reachable only through that closure. This is also why the wrapper needs *args, **kwargs: it doesn’t know in advance what signature the decorated function will have, so it must accept and forward anything.
Common Mistakes
Mistake 1: Forgetting functools.wraps
Without it, the wrapped function silently loses its identity — its __name__, __doc__, and other introspection metadata all become the wrapper’s, which breaks debugging, documentation tools, and anything relying on __name__.
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logger
def process():
"""Process the data."""
pass
print(process.__name__)
print(process.__doc__)
Output:
wrapper
None
The fix is to decorate the inner wrapper with @functools.wraps(func), which copies __name__, __doc__, and more from the original function onto wrapper:
import functools
def logger(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logger
def process():
"""Process the data."""
pass
print(process.__name__)
print(process.__doc__)
Output:
process
Process the data.
Mistake 2: A wrapper that doesn’t accept the original function’s arguments
If wrapper is written with a fixed (or empty) signature, the decorator only works for functions matching that exact signature, and breaks for anything else at call time:
def logger(func):
def wrapper():
print(f"Calling {func.__name__}")
return func()
return wrapper
@logger
def add(a, b):
return a + b
print(add(3, 4))
Output:
TypeError: wrapper() takes 0 positional arguments but 2 were given
add is now wrapper, which takes no parameters, so calling add(3, 4) raises a TypeError before func is ever reached. The fix is to always give wrapper a generic *args, **kwargs signature and forward those straight through:
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logger
def add(a, b):
return a + b
print(add(3, 4))
Output:
Calling add
7
Best Practices
- Always apply
@functools.wraps(func)to your innerwrapperso metadata (__name__,__doc__,__module__) and introspection tools keep working. - Give
wrapperthe signature(*args, **kwargs)unless you have a specific reason to restrict it — this keeps your decorator general-purpose. - Keep decorators focused on one concern (logging, timing, caching, validation) — combine several small decorators via stacking rather than writing one that does everything.
- Remember stacking order: decorators apply bottom-up (closest to the function first), so put the decorator that should run “innermost” directly above the function.
- Prefer the standard library when it already solves your problem —
functools.lru_cachefor memoization,functools.singledispatchfor type-based dispatch,contextlib.contextmanagerfor context-manager-producing generators. - If a decorator needs configuration arguments, structure it as a three-level decorator factory (
def repeat(times): def decorator(func): def wrapper(...): ...) rather than trying to overload a single level. - Avoid decorators with hidden side effects that surprise callers (like silently swallowing exceptions) unless that is explicitly the decorator’s documented purpose.
Practice Exercises
- Write a decorator named
uppercasethat converts a wrapped function’s string return value to uppercase. Apply it to a function that returns"python is fun"and print the result. Expected output:PYTHON IS FUN. - Write a parameterized decorator
retry(times)that calls the wrapped function up totimestimes, stopping as soon as it succeeds without raising an exception, and re-raising the last exception if all attempts fail. Test it on a function that fails the first two times it’s called and succeeds the third time. - Write two decorators,
boldanditalic, each wrapping a string-returning function’s output in<b>or<i>tags respectively. Stack them on a function returning"hello"so the output is<b><i>hello</i></b>, and explain in a comment why that particular stacking order produces that particular nesting.
Summary
- A decorator is a callable that takes a function and returns a (usually new) function —
@decoratorabove adefis exactlyfunc = decorator(func). - The typical shape is
decorator(func)returning an innerwrapper(*args, **kwargs)that callsfuncand adds behavior before/after. - Parameterized decorators need an extra outer layer: a factory function that accepts configuration and returns the actual decorator.
- Stacked decorators apply bottom-up — the one nearest the function runs innermost.
functools.wrapspreserves the original function’s name and docstring and should be used in virtually every decorator you write.- Common bugs come from omitting
*args, **kwargsinwrapper, or forgetting to return the inner function’s result.
