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]"
While we’re here should we also allow variance inference for TypeVarTuple? If we act fast we can still get runtime support for explicitly declaring variance into Python 3.15. I think @jorenham had a use case for non-invariant TypeVarTuple?
A general class of use-cases are that of variadic tuple wrappers, where you’d want the TypeVarTuple to be covariant to match the tuple variance. There are several of there in scipy-stubs:
In scipy-stubs there are also examples where TypeVarTuple is used exclusively for input types, e.g. for the pos-only params of a wrapped Callable signature: