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)
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, andAnimal -> Animalcan be used wherever aCat -> Animalwas expected.
In other words, the → type constructor is contravariant in the parameter (input) type and covariant in the return (output) type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With