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: