Decorators
Wrap functions with @ syntax, functools.wraps, decorators with arguments, caching and registration.
A decorator is a function that takes another function and returns a modified version of it. With the @ syntax, you can add logging, timing, caching, access control or retries to any function without touching its code. Frameworks use decorators everywhere — @app.get("/") in FastAPI, @pytest.fixture, @dataclass, @property — so understanding them demystifies a lot of Python.
Decorators build on two ideas you already know: functions are objects (they can be passed and returned), and closures (inner functions remember variables from their enclosing scope).
A decorator by hand#
shout receives the original function, defines a wrapper that calls it and modifies the result, and returns the wrapper. After greet = shout(greet), the name greet points to the wrapper — which still has access to the original through the closure variable func.
The @ syntax#
Writing greet = shout(greet) after every definition is clumsy, so Python offers syntactic sugar:
@shout above the def means exactly greet = shout(greet), applied immediately when the function is defined.
Handling any arguments#
The wrapper above only works for functions with no parameters. Use *args and **kwargs to accept and forward anything:
Always use functools.wraps
Without @functools.wraps(func), the decorated function would report its name as wrapper and lose its docstring — confusing in tracebacks, logs, help() and test reports. wraps copies __name__, __doc__, __module__ and more from the original, and stores the original as __wrapped__. Treat it as mandatory.
The standard decorator template#
Almost every decorator you write will look like this:
Practical example: timing#
Using try/finally means the timing is printed even if the function raises.
Decorators with arguments#
To write @retry(times=3), you need one more layer: retry(times=3) is called first, and it must return the actual decorator.
Three levels: retry (takes configuration) → decorator (takes the function) → wrapper (takes the call's arguments). Real retry code would also wait between attempts (time.sleep, often with exponential back-off).
Decorators that keep state#
Because the wrapper is a closure, it can keep state across calls — or you can attach attributes to it:
Built-in and standard-library decorators#
You've already met several:
@property,@classmethod,@staticmethod— method types.@dataclass— a class decorator that generates methods.@functools.cache/@functools.lru_cache(maxsize=...)— memoisation.@contextlib.contextmanager— turns a generator into a context manager.@functools.total_ordering— fills in comparison methods.
Caching is a dramatic example — the naive recursive Fibonacci becomes instant:
Without @cache, fib(100) would take longer than the age of the universe. Only cache pure functions (same input → same output, no side effects), and remember the cache holds every result in memory; use lru_cache(maxsize=1024) to bound it.
Stacking decorators#
Several decorators can be applied to one function. They're applied bottom-up (the one closest to def wraps first), and run top-down when called:
How frameworks use decorators#
A decorator doesn't have to wrap anything — it can just register the function and return it unchanged. That's how web frameworks map URLs to functions:
When you see @app.get("/users") in FastAPI or @app.route("/") in Flask, this is essentially what's happening.
Common mistakes#
- Forgetting to return the wrapper (or forgetting to return
resultinside it) — the decorated function becomesNoneor always returnsNone. - Calling the function in the decorator:
return wrapper()instead ofreturn wrapper. - Omitting
functools.wraps. - Writing
@retrywhen the decorator expects arguments — use@retry(), or design it to support both forms. - Caching impure functions with
@cache— you'll get stale results.
What's next#
You've now seen most of Python's core language. Next, a guided tour of the standard library: os, pathlib, datetime, collections, itertools and more.
Check your understanding
Quick quiz
1.What is
@my_decoratorabovedef greet():equivalent to?2.Why should a decorator's wrapper use
@functools.wraps(func)?3.How many levels of nested functions does a decorator that takes arguments, like
@retry(times=3), typically need?
Finished reading?
Mark this lesson complete to track your progress.