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

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

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

References