Code sample in basedpyright playground
from typing import Callable, Protocol, runtime_checkable
@runtime_checkable
class CanCall[**InP, OutT](Protocol):
def __call__(*args: InP.args, **kwargs: InP.kwargs) -> OutT: ...
def f1(takes_object: CanCall[[object], None]):
takes_int: CanCall[[int], None] = takes_object # Type parameter "InP@CanCall" is invariant, but "(object)" is not the same as "(int)"
def f2(takes_object: Callable[[object], None]):
takes_int: Callable[[int], None] = takes_object
as we know, Callable is not a special type, it is simply a protocol with a paramspec. but there is some magic special behaviour bestowed upon it to make it behave contravariantly
this missing feature of paramspecs is blocking the correction of the definitions of Callable:
additionally, this restriction is quite limiting for other usages, and is a stark contrast in behaviour to type variables
all of these issues are also present with type variable tuples:
Code sample in basedpyright playground
class A[OutT]:
def out(self) -> OutT: ...
a_int = A[int]()
a_obj: A[object] = a_int # okay
class B[*OutTs]:
def out(self) -> tuple[*OutTs]: ...
b_int = B[int]()
b_obj: B[object] = b_int # Type parameter "OutTs@B" is invariant, but "*tuple[int]" is not the same as "*tuple[object]"
specification update: