Skip to content

Decorators

Decorators are the cheapest way to attach new behavior in Python. They are also one of the easiest ways to destroy signatures, metadata, and debuggability if you treat wrappers casually.

Quick takeaway: a good decorator changes behavior while preserving the function's signature and metadata. `functools.wraps` and `ParamSpec` are close to mandatory defaults for serious decorator work.

Where a Decorator Acts

A decorator takes a callable or class, returns a new callable or class, and inserts extra behavior around the original contract.

Baseline Pattern With wraps and ParamSpec

py
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")


def traced(name: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            print(f"[{name}] before", func.__name__)
            result = func(*args, **kwargs)
            print(f"[{name}] after", func.__name__)
            return result

        return wrapper

    return decorator


@traced("calc")
def add(x: int, y: int) -> int:
    return x + y


print(add(1, 2))
print(add.__name__)

`wraps()` preserves metadata such as `__name__`, `__doc__`, and `__wrapped__`. `ParamSpec` keeps the wrapper aligned with the original call signature at type-check time.

Function vs Class Decorators

  • function decorators wrap or augment call behavior
  • class decorators post-process class objects
  • class decorators are often a lighter alternative to metaclasses

Overuse Patterns to Watch

Too much implicit behavior

Stacking retry, logging, tracing, auth, and transaction behavior across many decorators quickly hurts debuggability.

Metadata loss

Without `wraps()`, introspection, documentation, and framework integration can break.

Type degradation

Using `Callable[..., Any]` everywhere turns decorated code into a type-checking blind spot.

Stateful wrappers

Long-lived mutable state inside decorators can create concurrency and test-isolation problems.

Practical Connections

  • FastAPI route decorators
  • dependency helpers
  • logging, tracing, and caching wrappers

Official Sources

Built with VitePress for a Python 3.14 handbook.