Python ParamSpec: Type-Safe Decorators addresses a recurring problem in Python projects: Forward a complete callable signature through a higher-order function. This guide explains the mechanism, provides an executable example, and identifies the boundaries that keep an implementation reliable.

Concept and use case

ParamSpec captures positional and keyword parameters; TypeVar captures the result. Together they preserve the relationship between input and returned callables.

For the related fundamentals, also read the type hints guide. Integration stays simpler when functions receive dependencies and data explicitly instead of relying on global state.

Practical example

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

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

def traced(func: Callable[P, R]) -> Callable[P, R]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(func.__name__)
        return func(*args, **kwargs)
    return wrapper

Use functools.wraps for runtime metadata and Callable[P, R] for the static contract. Concatenate handles a wrapper that prepends one argument.

Important decisions

The correct choice depends on the public contract, expected volume, and failure behavior.

Consider concurrency, empty inputs, and partial failures. Document every limit that affects consumers and choose names that express intent.

Common mistakes

A minimal example does not replace bounds, error handling, and observability. Callable[..., R] loses argument checking. ParamSpec does not preserve names, docs, or runtime introspection by itself without wraps.

Avoid catching exceptions without context or returning partial output as if it were complete. An explicit failure is usually safer than silently incorrect data.

How to validate

Validate behavior, not only the happy path. Run a type checker against functions with different signatures and verify autocomplete, rejected arguments, and inferred return type.

The official documentation, accessed July 28, 2026, details the API and should remain the reference for future changes.