patternpythonnoneModerate
ParamSpec for Type-Safe Decorator Wrappers
Viewed 0 times
Python 3.10+ (typing_extensions for 3.8+)
ParamSpecdecorator typingtype-safe wrapperCallableparameter spec
Problem
Writing typed decorators that preserve the wrapped function's parameter types and return type is impossible with basic TypeVar — the decorator signature loses the original parameter types.
Solution
Use ParamSpec to capture and forward the original function's parameter types.
from typing import TypeVar, Callable, ParamSpec
from functools import wraps
import time
P = ParamSpec('P') # Captures parameter spec
R = TypeVar('R') # Captures return type
def timed(fn: Callable[P, R]) -> Callable[P, R]:
@wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f'{fn.__name__} took {elapsed:.3f}s')
return result
return wrapper
@timed
def process_data(items: list[str], *, batch_size: int = 100) -> dict[str, int]:
return {item: len(item) for item in items}
# Type checker knows:
result = process_data(['a', 'b'], batch_size=50) # OK
result = process_data(['a'], batch_size='bad') # Type error!Why
TypeVar alone can't express 'the same parameters as the wrapped function'. ParamSpec captures the full parameter specification (positional args and keyword args) and allows forwarding them correctly.
Gotchas
- ParamSpec requires Python 3.10+ or 'from __future__ import annotations' + typing_extensions for 3.8/3.9.
- Use P.args and P.kwargs in the wrapper's args/**kwargs annotations — not args: Any.
- Concatenate[X, P] adds a parameter to the front of P's signature.
Revisions (0)
No revisions yet.