pythoncontravariancesubtype

Why is `Callable` generic type contravariant in the arguments?


TL;DR:

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)


Context:

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?


More from the PEP 483:

The PEP 483 provides the following notions and definitions:

Subtype relationship:

Type subtype is a subtype of type basetype if:

Contravariance:

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.


My analysis attempt:

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:

  1. Every value for Callable[[basetype], None] would be included in the set of values of Callable[[subtype], None].
  2. Every function of 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.

1. Is every value for 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 type Callable[[Manager], None], providing an Employee to it would go against type safety and it's made obvious by the definition of the Manager.manage method. This means that despite being of type Callable[[Manager], None], my_callable isn't of type Callable[[Employee], None], which is enough to refute a systematic covariance.

2. Is every function of 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]?


Solution

  • 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.