Decorators
Functions that wrap other functions, and the @ syntax
A decorator is just a function that takes another function and returns a new one. The @decorator syntax is sugar for that pattern. Decorators power Flask's @app.route, Django's @login_required, dataclasses, @property, caching, logging, retry logic, and countless other patterns in production Python.
Real-world ubiquity
Open any modern Python codebase and you'll see decorators everywhere: `@app.route("/users")` in Flask/FastAPI, `@cache` for memoization, `@dataclass` to auto-generate boilerplate, `@staticmethod` and `@classmethod` in classes, `@timing` and `@retry` in production services. Understanding decorators means understanding how most Python frameworks work under the hood.
Closures recap
Before decorators, a quick reminder: a closure is a function that captures variables from its enclosing scope:
inner "closes over" x from outer. Even after outer returns, inner remembers x=5. Decorators use this pattern to wrap functions.
The minimal decorator
The @shout line is syntactic sugar for:
def greet(name):
return f"hello, {name}"
greet = shout(greet)The decorator shout takes the original greet, wraps it in a new function that uppercases the result, and replaces greet with that wrapper.
Preserve metadata with functools.wraps
Without wraps, the wrapped function loses its name, docstring, and signature:
Always use functools.wraps in real decorators:
ALWAYS use functools.wraps
Tools, debuggers, and stack traces rely on `name`, `doc`, and `module`. Without `@wraps(fn)`, your wrapper will show up as "wrapper" in tracebacks, and introspection tools (like `help()`) will fail. This is the #1 decorator mistake beginners make.
A useful example: timing
This pattern is everywhere in production: timing, logging arguments, checking permissions, retrying on failure, caching results.
Decorators with arguments (decorator factories)
If your decorator needs parameters, you need three levels of nesting. The outermost function takes the decorator's arguments and returns the real decorator:
Let's unpack what happens:
@repeat(times=3)callsrepeat(3), which returnsdecorator.- That
decoratoris applied tohi, wrapping it inwrapper. hiis now bound towrapper, which calls the originalhithree times.
Decorator-with-args gotcha
A decorator with arguments is a factory that returns a decorator. The nesting is:
``` repeat(times) → decorator → wrapper ```
Forgetting the middle layer is a common mistake. If you see "TypeError: decorator() takes 1 positional argument but 2 were given", you likely forgot the factory pattern.
Here's the desugared equivalent:
def hi():
print("hi")
decorator_instance = repeat(times=3) # Step 1: call the factory
hi = decorator_instance(hi) # Step 2: apply the decoratorStacking decorators
You can apply multiple decorators. They nest bottom-up (the closest to the function runs first):
Equivalent to:
greet = bracketed(excited(greet))The call stack is: bracketed wrapper → excited wrapper → original greet. So greet("Ada") returns "hello Ada", then excited adds !, then bracketed adds [], producing "[hello Ada!]".
Order matters
Stacking decorators bottom-up can be counterintuitive. The decorator closest to the function definition runs first (wraps the original function), and decorators above it wrap the result. If order matters (e.g., a `@cache` decorator should wrap `@retry`, not the other way around), pay attention to the order.
Class-based decorators
Sometimes you need state that survives across calls. A class with __call__ can act as a decorator:
The class instance replaces the original function. greet is now a CountCalls object, and calling it invokes __call__.
Decorating methods (mind self)
When decorating class methods, remember that the first argument is self:
The decorator sees self as the first positional argument in *args. This works fine for simple decorators. For more advanced patterns (like caching with self awareness), use libraries like functools.lru_cache or cachetools.
@staticmethod and @classmethod order
If you stack `@staticmethod` or `@classmethod` with other decorators, they must be outermost (applied last):
```python class Foo: @staticmethod @logged def bar(): pass ```
Applying `@staticmethod` first (closest to the function) can cause errors because `@staticmethod` returns a descriptor, not a callable function.
Built-in decorators worth knowing
@functools.wraps— use inside your own decorators to preserve metadata.@functools.cache(3.9+) — memoize function results indefinitely.@functools.lru_cache(maxsize=128)— least-recently-used cache with a size limit.@staticmethod,@classmethod,@property— for classes (covered in the Classes page).@dataclass(fromdataclasses) — auto-generates__init__,__repr__, etc.
Without @cache, computing fib(100) recursively would take millions of years. With it, every result is memoized, and the answer is instant.
Challenges
Define a decorator timing that prints how long a function took to run (in milliseconds) and returns the result. Use time.perf_counter() for timing and functools.wraps to preserve metadata.
Example output: add took 0.05 ms
Define a decorator factory retry(n) that retries a function up to n times if it raises an exception. If all attempts fail, let the exception propagate. Use functools.wraps.
Example: @retry(3) tries the function up to 3 times.
Define a decorator memoize that caches function results based on arguments. Store the cache in a dict on the wrapper. Use functools.wraps.
Assume all arguments are hashable. The cache should persist across calls.
Define a decorator only_positive that raises ValueError if any positional argument is not a positive number (> 0). Use functools.wraps.
Example: @only_positive on add(a, b) should raise if a <= 0 or b <= 0.
Multiple-choice questions
What is the effect of @functools.wraps(fn) on a wrapper function?
It makes the wrapper run faster.
It copies metadata (name, docstring, signature) from fn onto the wrapper.
It prevents the wrapper from accepting *args and **kwargs.
It is required syntax for the @ decorator notation to work.
Given:
@a
@b
def f(): ...
Which is equivalent?
f = a(b)(f)
f = a(b(f))
f = b(a(f))
f = a(f) + b(f)
What is a "decorator factory"?
A function that creates multiple decorators at once.
A function that takes arguments and returns a decorator.
A built-in Python module for generating decorators.
A decorator that only works on factory functions.
Why should you always use *args, **kwargs in a decorator wrapper?
To make the wrapper run faster.
To accept any arguments the wrapped function expects, without knowing them in advance.
It is required syntax for decorators to work.
To prevent the function from being called with keyword arguments.
What happens if you forget to return fn(*args, **kwargs) in a decorator wrapper?
The original function is called anyway.
The wrapped function always returns None, breaking its behavior.
Python raises a DecoratorError.
The decorator is ignored and the original function is used.
In a class-based decorator, what does __call__ do?
It is called when the decorator is applied to a function.
It is called each time the wrapped function is invoked, making the instance callable.
It registers the class as a decorator in Python's internal registry.
It is required syntax for class decorators to work.
You can now write any function you can imagine and bolt extra behavior onto it. Time to talk about how to ship that code: virtual environments and pip.