Python Decorators
A decorator is a function that takes a function (or class) and returns a modified version of it. The @ syntax applies it at definition time. Decorators are everywhere in Python: @app.get("/users") in FastAPI, @pytest.fixture, @dataclass, @property, @functools.cache. They let you attach cross-cutting behavior like caching, retries, logging, authorization, and registration without cluttering the function body.
The mechanism is simple once you see that functions are ordinary objects. @decorator is shorthand for func = decorator(func). Everything else follows from that.
TL;DR
@decoabovedef fmeansf = deco(f), run once, at definition time.- A typical decorator defines an inner wrapper function (a closure) that calls the original, and returns the wrapper.
- Always apply
@functools.wraps(fn)to the wrapper to preserve the name, docstring, and signature metadata. - Decorators with arguments (
@retry(times=3)) are factories: a function that returns a decorator. - Decorators can also target classes (
@dataclass) and methods (@property,@classmethod). - Type them with
ParamSpecso the decorated function keeps its signature for type checkers.
Quick Example
A timing decorator, and a retry decorator that takes arguments:
Decorators stack bottom-up: fetch_report = timed(retry(times=5, ...)(fetch_report)). The timing covers all retry attempts.
Core Concepts
Functions Are Objects
Functions can be assigned to variables, passed as arguments, returned from other functions, and have attributes. That's all a decorator needs:
Closures
The inner wrapper "closes over" fn. It keeps a reference to the variable from the enclosing scope even after shout has returned. Closures are also how decorator factories remember their arguments (times, exceptions above). To rebind an enclosing variable inside the wrapper, declare it nonlocal.
functools.wraps
Without it, the decorated function reports the wrapper's identity:
wraps copies __name__, __qualname__, __doc__, __module__, and __dict__, and sets __wrapped__ so tools like inspect.signature, pytest, and debuggers see the original function.
Decorators With Arguments
@retry(times=5) first calls retry(times=5), which returns the actual decorator, which is then applied to the function. That makes three levels: factory → decorator → wrapper. A common trick supports both @deco and @deco(opt=1) by checking whether the first argument is callable, but explicit factories are clearer.
Class and Method Decorators
- Class decorators receive the class and return it, modified or replaced:
@dataclassgenerates__init__,__repr__, and__eq__from annotations. - Built-in method decorators:
@property(computed attributes),@classmethod(receives the class),@staticmethod(no implicit first argument),@functools.cached_property. - A decorator can also be a class with
__call__, which is useful when the wrapper needs substantial state.
Registration Decorators
Not every decorator wraps. Many simply record the function and return it unchanged. This is how web frameworks build route tables and how plugin systems discover handlers:
Useful Standard-Library Decorators
Best Practices
Always Use functools.wraps
It costs one line and prevents confusing tracebacks, broken introspection, and frameworks that key on __name__ (for example, Flask endpoint names colliding as wrapper).
Keep Decorators Transparent
A decorator should preserve the wrapped function's contract: same arguments, compatible return value, and exceptions propagated unless handling them is the point. Surprising behavior hidden behind an @ is hard to debug.
Type Them With ParamSpec
Callable[P, R] with ParamSpec keeps full argument checking and editor autocomplete on decorated functions. Untyped decorators turn every decorated function into Callable[..., Any]. See Python type hints.
Handle Async Functions
A sync wrapper around an async def returns the coroutine without awaiting it, so timing or retry logic runs around nothing. Detect with inspect.iscoroutinefunction(fn) and provide an async def wrapper that awaits, or write separate decorators. See asyncio.
Common Mistakes
Calling the Function Instead of Returning the Wrapper
Forgetting the Parentheses on a Factory
@retry (no parentheses) passes the function as times, so the "decorator" you get back is a function that expects a function. It fails confusingly on first call. Use @retry() or support both forms explicitly.
Caching Methods With lru_cache
@lru_cache on an instance method caches on self too, keeping every instance alive for the cache's lifetime (a memory leak). Use cached_property, a per-instance cache, or cache a module-level function keyed by the data you need.
FAQ
When does decorator code run?
The decorator itself runs once, when the def statement executes, usually at import time. The wrapper's body runs on every call. Side effects in the decorator body (like registration) therefore happen on import.
What order are stacked decorators applied in?
Bottom-up. The decorator closest to def is applied first, and the topmost wraps everything. At call time, the topmost wrapper runs first. So @auth above @cache checks authorization before consulting the cache, which is usually what you want.
Can I decorate a class?
Yes. A class decorator receives the class object and returns a class, either the same one modified in place (adding methods or registering it) or a new one. @dataclass, @total_ordering, and @final are standard examples.
How do I test a decorated function without the decorator?
If the decorator used functools.wraps, the original is available as func.__wrapped__. Better still, test the decorator's behavior directly with a small dummy function, and test business logic in functions that don't depend on the decorator.
Related Topics
- Python — The language overview
- Python Type Hints — ParamSpec and typed wrappers
- Python Generators —
@contextmanagerand generator-based context managers - Design Patterns — The decorator pattern in object-oriented design
- FastAPI — Route registration through decorators