Python's fourth beta preserves decorator parameter types
Python 3.10.0b4 includes ParamSpec and Concatenate. These annotations describe how wrappers preserve or transform a callable's parameters without weakening every call to an ellipsis.
A decorator can preserve the way a function accepts arguments while its annotation loses that information. The wrapper forwards *args and **kwargs, but a return annotation such as Callable[..., int] tells a type checker nothing about which arguments are valid. Python 3.10.0b4, released on July 10, includes the parameter specification variables designed to express that relationship. The addition addresses a common gap between a wrapper's implementation and its static description.
This is still a beta preview, with production use discouraged. The release announcement encourages third-party maintainers to exercise their projects against it and states a goal of avoiding ABI changes after beta four. Alongside native compatibility work, the beta makes new typing constructs available for evaluating library annotations.
I find decorators useful when they preserve a function's ordinary calling behavior. Losing the signature in the type system weakens that convenience because the decorated function becomes harder to check. A mechanism that carries the original parameter contract through the wrapper is a practical improvement for library consumers.
An ellipsis removes an important constraint
Mark Mendoza's PEP 612 motivates the change with a logging decorator. Its initial annotation preserves the wrapped function's result type but uses an ellipsis for the parameters. That permits an invalid call to pass static checking even though forwarding the supplied arguments will fail at runtime.
An ordinary type variable can relate individual types across a function. A decorator needs a relationship between whole parameter specifications, including how the function accepts positional and keyword arguments. Enumerating one fixed list of argument types would make the decorator specific to that signature. Replacing the list with an ellipsis discards the requirement altogether.
ParamSpec represents those parameters as a reusable variable. A decorator can accept a callable parameterized by P and return another callable using the same P. Its result type can remain the same or change independently.
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def as_text(function: Callable[P, R]) -> Callable[P, str]:
def wrapped(*args: P.args, **kwargs: P.kwargs) -> str:
return str(function(*args, **kwargs))
return wrapped
@as_text
def total(count: int, *, unit: int = 1) -> int:
return count * unit
result = total(3, unit=2)
# A supporting type checker can preserve these parameters and infer str.
The wrapper changes the result to text while leaving the wrapped function's accepted arguments intact. Under the PEP's rules, a supporting checker can reject a call with an unsuitable argument type. Merely importing the runtime objects does not establish that every installed type checker implements those rules, so interpreter support and checker support remain separate requirements.
The two components must stay related
Python calls divide arguments between a positional tuple and keyword mapping. A parameter specification covers both, while P.args and P.kwargs describe the corresponding components for forwarding. PEP 612 requires their coordinated use in *args and **kwargs annotations.
This is more precise than describing every positional argument as object and every keyword value as object. Such broad annotations lose the relationships between particular positions or names and their types. The parameter specification retains the callable's own requirements across the forwarding operation.
A parameter may also be supplied positionally in one call and by keyword in another. The PEP uses that fact to explain why independently capturing unrelated positional and keyword specifications is insufficient. The specification has to preserve the valid ways of calling the function, rather than pick one partition and assume it describes every invocation.
That restriction also limits where the component annotations make sense. P.args is not a general-purpose type for a stored argument collection, and P.kwargs is not an unrestricted replacement for a dictionary type. They describe the coordinated forwarding relationship established by the surrounding callable annotation.
Some wrappers add a leading argument
PEP 612 also introduces Concatenate. A wrapper that supplies an initial request object to a function needs to describe that extra parameter without exposing it to callers of the wrapper. Callable[Concatenate[Request, P], R] expresses a callable whose first positional parameter is a request and whose remaining parameters are described by P.
The wrapper can return Callable[P, R] after supplying that leading argument itself. Conversely, a transformation can add a leading parameter to the returned callable. These operations describe a fixed positional prefix, and the PEP deliberately excludes arbitrary changes to keyword-only parameters.
The positional restriction prevents collisions with names already present in P. If a decorator injects a keyword under a name that the original function also accepts, forwarding can produce multiple values for the same argument. Concatenate avoids promising a general solution to that problem by defining its added parameters positionally.
I prefer this bounded description to an annotation that appears exact but cannot account for real calling conventions. It gives signature-preserving wrappers a useful contract and states where the model stops. A decorator that performs a more complicated transformation may still need a different annotation strategy.
For library authors examining the beta, the relevant question is whether a wrapper's annotation describes the same calls its implementation forwards. ParamSpec makes that correspondence expressible without enumerating every wrapped signature. A callable's return type can change while its parameter requirements remain available to the checker and to the code that consumes the decorated function.