pythongenericstypesmypystructural-typing

Generic protocols: mypy error: Argument 1 has incompatible type ...; expected


I'm trying to implement a generic Protocol. My intent is to have a Widget[key_type, value_type] protocol with a simple getter. Mypy complained about Protocol[K, T] so that became Protocol[K_co, T_co]. I've already stripped out all the other constraints, but I can't even get the most basic situation, widg0: Widget[Any, Any] = ActualWidget(), to work. ActualWidget.get should be totally compatible with get(self, key: K) -> Any, which makes me think I'm using the generics/protocol wrong in some way, or mypy just can't handle this.

command/error from mypy:

$ mypy cat_example.py
cat_example.py:34: error: Argument 1 to "takes_widget" has incompatible type "ActualWidget"; expected "Widget[Any, Any]"
cat_example.py:34: note: Following member(s) of "ActualWidget" have conflicts:
cat_example.py:34: note:     Expected:
cat_example.py:34: note:         def [K] get(self, key: K) -> Any
cat_example.py:34: note:     Got:
cat_example.py:34: note:         def get(self, key: str) -> Cat
Found 1 error in 1 file (checked 1 source file)

or alternatively, if I try to force the assignment with widg0: Widget[Any, Any] = ActualWidget():

error: Incompatible types in assignment (expression has type "ActualWidget", variable has type "Widget[Any, Any]")

The full code:

from typing import Any, TypeVar
from typing_extensions import Protocol, runtime_checkable

K = TypeVar("K")  # ID/Key Type
T = TypeVar("T")  # General type
K_co = TypeVar("K_co", covariant=True)  # ID/Key Type or subclass
T_co = TypeVar("T_co", covariant=True)  # General type or subclass
K_contra = TypeVar("K_contra", contravariant=True)  # ID/Key Type or supertype
T_contra = TypeVar("T_contra", contravariant=True)  # General type or supertype

class Animal(object): ...

class Cat(Animal): ...


@runtime_checkable
class Widget(Protocol[K_co, T_co]):
    def get(self, key: K) -> T_co: ...

class ActualWidget(object):
    def get(self, key: str) -> Cat:
        return Cat()

def takes_widget(widg: Widget):
    return widg

if __name__ == '__main__':
    widg0 = ActualWidget()
    #widg0: Widget[str, Cat] = ActualWidget()
    #widg0: Widget[Any, Any] = ActualWidget()

    print(isinstance(widg0, Widget))
    print(isinstance({}, Widget))
    takes_widget(widg0)

Solution

  • Putting what I had in the comments here.

    To make your question's example work, you need to make the input parameter contravariant and the output parameter covariant like so:

    from typing import TypeVar
    from typing_extensions import Protocol, runtime_checkable
    
    T_co = TypeVar("T_co", covariant=True)  # General type or subclass
    K_contra = TypeVar("K_contra", contravariant=True)  # ID/Key Type or supertype
    
    class Animal: ...
    
    class Cat(Animal): ...
    
    @runtime_checkable
    class Widget(Protocol[K_contra, T_co]):
        def get(self, key: K_contra) -> T_co: ...
    
    class ActualWidget:
        def get(self, key: str) -> Cat:
            return Cat()
    
    def takes_widget(widg: Widget):
        return widg
    
    class StrSub(str):
        pass
    
    if __name__ == '__main__':
        widget_0: Widget[str, Cat] = ActualWidget()
        widget_1: Widget[StrSub, Cat] = ActualWidget()
        widget_2: Widget[str, object] = ActualWidget()
        widget_3: Widget[StrSub, object] = ActualWidget()
    
        takes_widget(widget_0)
        takes_widget(widget_1)
        takes_widget(widget_2)
        takes_widget(widget_3)
    

    ActualWidget(), which is a Widget[str, Cat], is then assignable to Widget[SubStr, object] for widget_3, meaning that Widget[str, Cat] is a subclass of Widget[SubStr, object].

    Widget[str, Cat] can take all SubStrs plus other str subtypes (the input type in the sublcass relationship can be less specific, hence contravariance) and can have an output that is atleast an object, plus having str properties (the output type in the subclass relationship can be more specific, hence covariance). See also Wikipedia - Function Types, which formalizes this observation:

    For example, functions of type Animal -> Cat, Cat -> Cat, and Animal -> Animal can be used wherever a Cat -> Animal was expected.

    In other words, the → type constructor is contravariant in the parameter (input) type and covariant in the return (output) type.