We ran into some issues while trying to use ParamSpec
to type lru_cache
in typeshed. See
Basically, while self
/ cls
is bound in case the ParamSpec
is directly passed through as Callable
, that isn’t the case for the generic version.
from typing import Callable, Generic, ParamSpec
P = ParamSpec("P")
class Wrapper(Generic[P]):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> None: ...
def decorator(f: Callable[P, None]) -> Callable[P, None]: ...
def lru_cache(f: Callable[P, None]) -> Wrapper[P]: ...
class A:
@decorator
def method1(self, val: int) -> None: ...
@lru_cache
def method2(self, val: int) -> None: ...
def test(self) -> None:
reveal_type(self.method1) # def (val: int)
reveal_type(self.method2) # Wrapper[[self: A, val: int]]
self.method1(2)
self.method2(2) # Error: Missing positional argument "val"
Although all type checkers seem to agree, this makes using generic ParamSpec
in these cases difficult (if not impossible). One option would be to remove the first argument with Concatenate
and using overloads but that creates additional issues with @staticmethod
and @classmethod
.
The solution IMO would be to allow self binding even for generic ParamSpec, so that it doesn’t matter if the return type is a Callable
or a custom generic. I.e.
def test(self) -> None:
reveal_type(self.method1) # def (val: int)
reveal_type(self.method2) # Wrapper[[val: int]]
self.method1(2)
self.method2(2)
A reference implementation for mypy can be found here: