Python

Python Decorators👨‍💻

Decorators are Python's most elegant metaprogramming feature. The @ syntax lets you wrap any callable to inject behavior — logging, caching, access control, retries — without touching the original function's code. Under the hood, @decorator is just syntactic sugar for func = decorator(func), but that one-line abstraction powers everything from Flask routes to dataclasses to pytest fixtures.

Key Takeaways

  • 1A decorator is a callable that takes a function and returns a modified version of it — the `@` syntax is sugar for `func = decorator(func)`
  • 2Always use `@functools.wraps(func)` inside your wrapper to preserve the original function's `__name__`, `__doc__`, `__annotations__`, and `__wrapped__` reference
  • 3Parametrized decorators require three nested functions: the outer factory takes arguments, returns the actual decorator, which returns the wrapper
  • 4Stacking decorators applies bottom-up — `@a @b @c def f` becomes `f = a(b(c(f)))` — so the decorator closest to the function runs first
  • 5Class decorators receive the class itself and can inject methods, enforce invariants, or register classes in a registry — `@dataclass` is the canonical example
  • 6The `func=None` sentinel pattern lets you write decorators that work both with and without parentheses: `@retry` and `@retry(times=5)`

Master python decorators

Take the Python Fundamentals course with hands-on lessons and challenges.

Examples

Timing decorator — the universal debugging tool

python

Uses try/finally so the elapsed time is logged even if the function raises. Logging instead of print means this works in production without cluttering stdout. The perf_counter clock has nanosecond resolution.

Retry with exponential backoff

python

The func=None sentinel pattern makes the decorator flexible: @retry works without parentheses, and @retry(max_attempts=5) works with them. The exponential backoff multiplies the delay after each failure. Filtering on specific exception types prevents retrying on bugs like TypeError.

Memoization cache with TTL

python

Builds on the same idea as functools.lru_cache but adds time-based expiration. The cache key is built from positional and keyword arguments. Attaching cache_clear to the wrapper gives callers a way to invalidate stale data — the same pattern lru_cache uses. For production, consider functools.lru_cache or cachetools instead of rolling your own.

Authentication decorator for Flask/FastAPI routes

python

A parametrized decorator that checks for a JWT token and optionally enforces a role. Notice the stacking order: @app.route must come before @require_auth so Flask registers the wrapped function. The decorator sets g.current_user, making the authenticated user available downstream without polluting function signatures.

Rate limiter with sliding window

python

Uses a deque as a sliding window of call timestamps. Old entries are evicted on each call, and if the window is full, the decorator raises instead of silently blocking. In production you would swap the deque for a Redis-backed counter, but the decorator interface stays identical.

Validate arguments with a class decorator

python

A class-based decorator that uses inspect.signature to bind arguments and check them against type annotations. Using functools.update_wrapper (the class equivalent of @functools.wraps) preserves the original function's metadata. This pattern is how libraries like pydantic and beartype work under the hood.

Common Mistakes

Mistake:

Forgetting `@functools.wraps(func)` — the decorated function loses its name, docstring, and type annotations, which breaks introspection tools, API docs generators, and debugging

Fix:

Always apply `@functools.wraps(func)` to your inner wrapper function. It copies `__name__`, `__doc__`, `__annotations__`, `__module__`, and sets `__wrapped__` so you can access the original function.

Mistake:

Forgetting to return the result of the wrapped function — writing `func(*args, **kwargs)` without `return`, so the decorated function silently returns `None`

Fix:

Always write `result = func(*args, **kwargs)` then `return result` (or just `return func(*args, **kwargs)` directly). This is the most common decorator bug and it produces no error — just wrong values downstream.

Mistake:

Getting the stacking order wrong — placing `@app.route` below `@require_auth` so Flask registers the unwrapped function and auth is never checked

Fix:

Remember that decorators apply bottom-up. The outermost decorator (top of the stack) wraps everything below it. For Flask: `@app.route` on top, then `@require_auth` below, so the route handler is the authenticated version.

Mistake:

Using mutable default arguments in the decorator factory — e.g. `def decorator(func, cache={})` — which shares state across all decorated functions

Fix:

Create mutable state (dicts, lists, deques) inside the decorator or wrapper scope, not as default arguments. Each decorated function should get its own isolated state unless sharing is intentional.

Best Practices

  • Always use `@functools.wraps(func)` — no exceptions. It costs nothing and prevents subtle bugs with serialization, documentation generators, and testing frameworks that rely on `__name__`
  • Prefer the `func=None` sentinel pattern for decorators that may or may not take arguments — it gives callers the cleanest possible API: `@retry` and `@retry(times=5)` both work
  • Keep decorator logic thin. If the wrapper body grows past 15 lines, extract the behavior into a separate function and have the decorator just call it — decorators should compose behavior, not implement it
  • Use `time.perf_counter()` for timing decorators and `time.monotonic()` for rate limiters and TTL caches — never `time.time()` which can jump on clock adjustments
  • Write decorators that are transparent to type checkers: use `ParamSpec` and `TypeVar` from `typing` (Python 3.10+) so decorated functions keep their original signatures in IDE autocomplete and mypy
  • Test decorators by accessing `func.__wrapped__` to call the original function directly — this isolates the function logic from the decorator logic in your test suite

Summary

Decorators are functions that wrap other functions to inject cross-cutting behavior — timing, caching, auth, retries, validation — without modifying the original code. The `@` syntax is sugar for reassignment. Always use `functools.wraps` to preserve metadata. Parametrized decorators use a three-level nesting pattern (factory, decorator, wrapper). Class-based decorators store state between calls. Stacking applies bottom-up. The best decorators are thin, composable, and transparent to type checkers.

Practice Python with hands-on challenges

Learn python decorators hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Python with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.