I've encountered the following (error in 2nd snippet) while experimenting with type hinting:
from typing import Callable
T = TypeVar("T") # Invariant by default
P = ParamSpec("P")
def some_decorator(f: Callable[P, T]): ... # Passes type checking
from typing import Callable, TypeVar, ParamSpec
T = TypeVar("T") # Invariant by default
P = ParamSpec("P")
MyCallableAlias = Callable
# error: Bad number of arguments for type alias, expected 0, given 2 [type-arg]
def my_decorator(f: MyCallableAlias[P, T]): ...
I don't really understand this as I would have expected MyCallableAlias
to behave exactly like Callable
.
The thing that seems to work is to use a Protocol
:
from typing import Protocol, TypeVar, ParamSpec, Generic
# Using the default variance (invariant) causes an error at type checking.
T = TypeVar("T", covariant=True)
P = ParamSpec("P")
class MyCallableAlias(Generic[P, T], Protocol):
def __call__(self, *args: P.args, **kwargs: P.kwargs): ...
def my_decorator(f: MyCallableAlias[P, T]): ...
Why didn't my alias work in the second example?
My solution seems over-complicated for a simple alias (actually not an alias anymore) compared to the use of what it's an alias for.
Note :
The tools and versions used for these examples were:
It is just how things are defined. Callable
in MyCallableAlias = Callable
means Callable[..., Any]
.
You need to make MyCallableAlias
generic too:
P = ParamSpec('P')
R = TypeVar('R')
MyCallableAlias = Callable[P, R]
def some_decorator(f: MyCallableAlias[P, T]) -> MyCallableAlias[P, T]:
...
In 3.12+, the syntax is more intuitive:
type MyCallableAlias[**P, R] = Callable[P, R]