A decorator is a function that takes a function and returns a function. That is the entire idea, and once you see it as an ordinary call rather than new syntax, @ stops being magic. What follows builds one from a single def, then fixes the two problems that appear along the way.
What is a decorator, really?
Write the decoration out longhand and the syntax disappears. Below, shout is a plain function; announce is a decorator; and add = announce(add) is what the @ symbol expands to.
def announce(func):
"""Print a message before and after calling func."""
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
result = func(*args, **kwargs)
print(f"returned {result}")
return result
return wrapper
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
add = announce(add)
print(add(2, 3))
Three details do the work. The inner function accepts *args and **kwargs so the wrapper works with any signature. The result of the inner call is returned, so the decorated function still behaves like the original. And the wrapper closes over func - which is a closure, the subject of the previous lesson.
With the syntax written out, the decorator reads as what it is.
def announce(func):
"""Print a message before and after calling func."""
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
result = func(*args, **kwargs)
print(f"returned {result}")
return result
return wrapper
@announce
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
print(add(2, 3))
Why does functools.wraps matter?
The wrapper replaces the original function, and the replacement carries none of its identity. Its name becomes wrapper, its docstring disappears and its signature is (*args, **kwargs). Any tool that inspects functions - your debugger, help(), a test runner, a documentation generator - now sees the wrapper.
from functools import wraps
def announce(func):
"""Print a message before and after calling func."""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
result = func(*args, **kwargs)
print(f"returned {result}")
return result
return wrapper
@announce
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
print(add.__name__)
print(add.__doc__)
@wraps(func) copies __name__, __doc__, __module__, __qualname__ and the signature metadata onto the wrapper, and sets __wrapped__ so that inspect.signature reports the original signature. Without it, add.__name__ is wrapper and add.__doc__ is None.
Use @wraps on every decorator you write, without exception. It costs one import and one line, and it is the difference between a decorator that hides your function and one that preserves it.
How do you write a decorator that takes arguments?
A decorator that is configured - "repeat three times", "retry five times", "log to this logger" - needs one extra layer. The outer function is called with the configuration and must return the real decorator.
from functools import wraps
def repeat(times: int):
"""Return a decorator that runs the decorated function several times."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name: str) -> str:
"""Return a greeting."""
return f"Hello, {name}"
print(greet("Ada"))
Three nested functions, each with one job: repeat captures the configuration, decorator captures the function, and wrapper does the work. This shape - a factory returning a decorator returning a wrapper - is the pattern behind most decorator libraries, including pytest fixtures and Flask route decorators.
The same three-layer shape works for a decorator that transforms the return value rather than the call. A cache, a retry loop, a validator - all of them can be written as "call the function, then decide what to do with the result and the exception".
Which decorators are worth knowing by heart?
Besides your own, a short list covers most of what you will meet in real code.
from functools import lru_cache, wraps
from time import perf_counter
def timed(func):
"""Print how long the decorated call took."""
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} took {perf_counter() - start:.6f}s")
return result
return wrapper
@timed
def sum_of_squares(n: int) -> int:
"""Return the sum of the first n squares."""
return sum(i * i for i in range(n))
@lru_cache(maxsize=128)
def square(n: int) -> int:
"""Return n squared, remembering previous answers."""
return n * n
print(sum_of_squares(100000))
print(square(12), square(12))
print(square.cache_info())
functools.lru_cache is a decorator you will use constantly: it remembers the arguments and their results, so a repeated call returns instantly. The cache_info() output shows the hits and misses, which is the quickest way to tell whether a cache is worth keeping.
The trio from the object-oriented lessons - @property, @classmethod and @staticmethod - are decorators too, and they follow exactly this pattern: they receive a function and return a different kind of object. @property turns a method into a computed attribute; @classmethod and @staticmethod change how the method receives its arguments.
One trap catches people who write their first decorator: forgetting to return the result. If the wrapper calls the function but does not return its value, the decorated function quietly returns None every time. The same applies to the wrapper itself - a decorator must return the wrapper, or the decorated name ends up bound to None and every later call fails with "NoneType is not callable".
Decorators also work on classes and on methods, but there is a subtlety with inheritance: decorating a method means the wrapper replaces the function on the class, so self arrives as the first positional argument inside the wrapper. That is why wrappers take *args rather than a named first parameter - the decorator does not need to know anything about the signature it wraps.
How do stacked decorators run?
When you stack decorators, they are applied from the bottom up, and the resulting functions run from the top down. The decorator closest to the function wraps it first.
from functools import wraps
def trace(label: str):
"""Return a decorator that prints a trace line."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{label}] entering")
result = func(*args, **kwargs)
print(f"[{label}] leaving")
return result
return wrapper
return decorator
@trace("outer")
@trace("inner")
def work() -> str:
"""Do some work."""
return "done"
print(work())
The output order is outer-entering, inner-entering, inner-leaving, outer-leaving. The inner decorator is applied first, so it wraps the raw function; the outer decorator then wraps that wrapper. Every layer adds a little behaviour and passes the result along.
Reading stacked decorators from the bottom up tells you the order in which the behaviour is nested. Reading from the top down tells you the order in which the reader encounters it - which is why the top decorator should be the one that describes the outermost concern, such as authentication around a route, rather than a low-level detail such as logging.
Key takeaways
- A decorator is a function that takes a function and returns a function - nothing more.
- Always use
@functools.wrapsso the decorated function keeps its name, docstring and signature. - A configurable decorator needs three layers: factory, decorator, wrapper.
- Stacking applies bottom-up and runs top-down; the order in the output matches that.
- Write your own decorator only after checking whether
lru_cache,propertyor the standard library already does the job.
Try it yourself: Write a @log_calls decorator that prints the function name and its arguments before each call. Add an optional prefix as an argument - @log_calls(prefix="app") - which means building the three-layer version. Stack it with the @trace decorator from this page and confirm the print order matches the explanation above. Finally, decorate a function with @lru_cache as well and check with cache_info() that the hits counter climbs.