ParamSpec captures function parameter types. Concatenate adds parameters.
1from typing import ParamSpec, TypeVar, Callable2from typing import Concatenate34P = ParamSpec("P")5R = TypeVar("R")67# Decorator that preserves signature8def log_calls(func: Callable[P, R]) -> Callable[P, R]:9 def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:10 print(f"Calling {func.__name__}")11 return func(*args, **kwargs)12 return wrapper1314@log_calls15def add(a: int, b: int) -> int:16 return a + b1718add(1, 2) # Type checker knows this is (int, int) -> int1920# Concatenate — add parameters21def with_request(22 func: Callable[Concatenate[Request, P], R]23) -> Callable[P, R]:24 def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:25 request = get_current_request()26 return func(request, *args, **kwargs)27 return wrapper2829@with_request30def get_user(request: Request, user_id: int) -> User:31 return User.objects.get(id=user_id)3233get_user(42) # Request is injected, type checker knows!
Use cases: