Why is the Callable
generic type contravariant in the arguments as stated by the PEP 483 and how is my analysis of that question (in)accurate? (Said analysis at the bottom of the post)
I'm currently studying topics about use of annotations for type analysis (MyPy, Pyright...). And I was reading the PEP 483 – The Theory of Type Hints that is related.
This PEP states the following in the part dedicated to _Covariance and contravariange:
One of the best examples to illustrate (somewhat counterintuitive) contravariant behavior is the callable type. It is covariant in the return type, but contravariant in the arguments.
This is what I'm trying to understand : why are callables contravariant in the arguments?
The PEP 483 provides the following notions and definitions:
Type subtype
is a subtype of type basetype
if:
subtype
is also in the set of values of basetype
; andbasetype
is also in the set of functions of subtype
.If subtype
is a subtype of basetype
, then a generic type constructor GenType
is called "contravariant" if GenType[basetype]
is a subtype of GenType[subtype]
for all such subtype
and basetype
.
The question "Why is Callable
contravariant in the arguments?" could be rephrased as:
With type subtype
being a subtype of type basetype
, why is Callable[[basetype], None]
a subtype of Callable[[subtype], None]
?
Which would imply, according to the above content from PEP 483, that:
Callable[[basetype], None]
would be included in the set of values of Callable[[subtype], None]
.Callable[[subtype], None]
would also be in the set of functions of Callable[[basetype], None]
.Answering my main question would be answering why these two conditions are verified.
Callable[[basetype], None]
included in the set of values of Callable[[subtype], None]
?subtype
being a subtype of basetype
, every value of type subtype
is therefore a value of type basetype
.
Therefore, any callable taking an argument of type basetype
will also accept an argument of type subtype
.
Which means that any value (callable value) of type Callable[[basetype], None]
is also a value of type Callable[[subtype], None]
.
Therefore, yes: every value of Callable[[basetype], None]
is also a value of the set of values of Callable[[subtype], None]
.
NB: The contrary (covariance one could naively expect as I did first) can be not possible.
let's define the following from the PEP 483's own examples:
class Employee: ... class Manager(Employee): def manage(self): ... def my_callable(m: Manager): m.manage()
Here, despite
my_callable
being of typeCallable[[Manager], None]
, providing anEmployee
to it would go against type safety and it's made obvious by the definition of theManager.manage
method. This means that despite being of typeCallable[[Manager], None]
,my_callable
isn't of typeCallable[[Employee], None]
, which is enough to refute a systematic covariance.
Callable[[subtype], None]
included in the set of functions of Callable[[basetype], None]
?I'm less sure about this question and how to answer it.
Let's define the following function taking a callable as a parameter:
def my_func(c: Callable[[subtype], None]): ...
Would any function with such a signature (Callable[[Callable[[subtype], None]], None]
) be a function of type Callable[[Callable[[basetype], None]], None]
?
Would any function accepting a Callable[[subtype], None]
also accept a Callback[[basetype], None]
? (I suppose of all that boils down to that question).
I suppose that any such function would pass to it's callable parameter a subtype
instance, and that therefore a Callable[[basetype], None]
would accept such subtype
instance which would make passing a Callable[[basetype], None]
OK.
But is it really enough to assert that every function such as my_func
would also belong to the set of functions of type Callable[[Callable[basetype], None], None]
?
Say you have a function f : Callable[[B], int]
, and three classes
class A: pass
class B(A): pass
class C(B): pass
f
can accept instances of B
or C
as its argument. It cannot accept an instance of A
.
Now consider
def f(g: Callable[[B], int) -> int:
b = B()
return g(b)
What kind of function can you pass to f
? Clearly, a Callable[[B], int]
will work, as it can take b
as an argument. But what about one of type Callable[[C], int]
? It cannot take b
as an argument, because C
is a subtype of B
. Thus, Callable[[C], int]
is not a subtype of Callable[[B], int]
.
You can, however, pass a function of type Callable[[A], int]
, because b
is not just an instance of B
, but also of A
. Thus, Callable[[A], int]
is a subtype of Callable[[B], int]
, because B
is a subtype of A
. That's all contravariance is.