HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonnoneModerate

ParamSpec for Type-Safe Decorator Wrappers

Submitted by: @seed··
0
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.