PEP 612 in 90 seconds — decorators that don't eat your signature
Wrap a function — keep every argument it had.
A decorator that wraps a function used to lose its signature. The wrapper became `(*args, **kwargs)` in your IDE and in mypy: ```python @log_args def greet(name: str, times: int = 1) -> str: ... # Pre-PEP-612 IDE view: greet(*args, **kwargs) — useless # Post-PEP-612 IDE view: greet(name: str, times: int = 1) -> str ``` FastAPI dependencies, retry decorators, cache layers — every modern decorator library leans on this fix.
PEP 612 (Python 3.10, 2021) added `ParamSpec` to typing. It binds the **entire parameter list** (both `*args` AND `**kwargs`) as one variable, so a decorator can declare "my wrapper takes the same params as the wrapped function and returns the same type": ```python P = ParamSpec("P") R = TypeVar("R") def log_args(fn: Callable[P, R]) -> Callable[P, R]: def inner(*args: P.args, **kwargs: P.kwargs) -> R: ... return inner ``` The runtime is unchanged. The typing-layer is rich now — autocomplete restored, type errors caught at the call site, refactors safe.
from typing import Callable, Any
def log_args(fn: Callable[..., Any]) -> Callable[..., Any]:
def inner(*args, **kwargs):
print(f"args={args}, kwargs={kwargs}")
return fn(*args, **kwargs)
return inner
@log_args
def greet(name: str, times: int = 1) -> str:
return name * times
# mypy sees: greet(*args: Any, **kwargs: Any) -> Any
# Type errors at call site? Not caught. IDE help? Gone.from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def log_args(fn: Callable[P, R]) -> Callable[P, R]:
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"args={args}, kwargs={kwargs}")
return fn(*args, **kwargs)
return inner
@log_args
def greet(name: str, times: int = 1) -> str:
return name * times
# mypy now sees: greet(name: str, times: int = 1) -> str
# Type errors caught. IDE autocomplete restored.🎯 Predict the output
What does this print? Runtime is unchanged — ParamSpec is purely a typing annotation.
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def log_args(fn: Callable[P, R]) -> Callable[P, R]:
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"args={args}, kwargs={kwargs}")
return fn(*args, **kwargs)
return inner
@log_args
def greet(name: str, times: int = 1) -> str:
return name * times
print(greet("hi ", times=3))