Types
ParamSpec
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
erasedParamSpec 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
loggedThe 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
5Notes
ParamSpecpreserves a callable's parameter list through transparent wrappers.- Pair
ParamSpecwith aTypeVarwhen the return type should also be preserved. - If the wrapper changes the public signature, write that new signature directly instead.
See also
- related: Callable Types
- prerequisite: Decorators
- related: Generics and TypeVar
Run the complete example
Expected output
calling add
5
calling add
5
Execution time appears here after you run the example.