Parameter specifications should have variance

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:

2 Likes