Skip to main content
🪞
PEP 612 · Speedrun · Python 3.10 (2021)

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.

Before — Callable[..., Any] erases the signature
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.
After — ParamSpec preserves the full signature
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))
Advanced typing & generics → Senior track

Or speedrun another PEP

PEP 572Assignment as an expression — 3 lines become 1.
PEP 572 in 90 seconds — the walrus that ate your for-loop
PEP 484Python stays dynamic — but you can opt into typing.
PEP 484 in 90 seconds — type hints, without TypeScript-envy
PEP 622Switch statements are a 5% feature. Pattern matching is the 95%.
PEP 622 in 90 seconds — match / case isn't just a switch