Types

ParamSpec

ParamSpec preserves callable parameter types through wrappers.

Callable[..., R] is sometimes too broad. It preserves the return type, but the ellipsis means the callable accepts any argument list as far as the type checker can tell.

Source

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

R = TypeVar("R")


def erased(func: Callable[..., R]) -> Callable[..., R]:
    return func

print(erased.__name__)

Output

erased
f(P)@DECP preservedwrapper(P)
ParamSpec preserves the wrapped function's signature through a decorator, parameter for parameter.

ParamSpec captures the original parameters and lets the wrapper forward exactly that shape.

Source

P = ParamSpec("P")


def logged(func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

print(logged.__name__)

Output

logged

The decorated function still runs normally. The benefit is static: tools can keep checking that add receives two integers.

Source

@logged
def add(left: int, right: int) -> int:
    return left + right

print(erased(add)(2, 3))
print(add(2, 3))

Output

calling add
5
calling add
5

Notes

See also

Run the complete example

Example code

Expected output

calling add
5
calling add
5

Execution time appears here after you run the example.