after having read up on covariance and contravariance within python I still find myself struggling to understand why an Invariant has to be made a contravariant to be used within the context of a protocol and I was hoping someone could further explain this concept to me. For example.
Let's assume the following:
from typing import Literal, Protocol, TypeVar
MyType = Literal["literal_1"]
G = TypeVar("G")
class MyProtocol(
Protocol[
G
],
):
@staticmethod
def do_work(message: G):
raise NotImplementedError
class CustomClass(
MyProtocol[
MyType
]
):
@staticmethod
def do_work(message: MyType):
pass
literal_1: MyType = "literal_1"
CustomClass.do_work(literal_1)
This will yield The following error using pyright/mypy:
warning: Type variable "G" used in generic protocol "MyProtocol" should be contravariant (reportInvalidTypeVarUse)
Changing the function to return a Generic of the same type:
def do_work(message: G) -> G:
raise NotImplementedError
@staticmethod
def do_work(message: MyType) -> Mytype:
return message
This error disappears.
I have read multiple sources that will paraphrase the following:
The short explanation is that your approach breaks subtype transitivity; see this section of PEP 544 for more information.
https://www.python.org/dev/peps/pep-0544/#overriding-inferred-variance-of-protocol-classes
I have read the section and am still confused as to why this error is being thrown for this particular example. Additionally I am confused as to why covariance is needed when a return type is given for a function defined in a protocol.
Imagine if you had a
BaseandDerivedclass, and an object conforming toMyProtocol[Base]. You should be allowed to pass that object to a function expecting aMyProtocol[Derived], since in that function, they would callthe_object.do_work(Derived()), which is a valid thing to pass to the version that expects aBase, sinceDerivedis a subclass ofBase. Hence, the more genericMyProtocol[Base]is valid wherever the more specificMyProtocol[Derived]is, so the type parameter is contravariant (and should be designated as such).Generally type parameters used as function parameters should be contravariant, since a function with more general parameter types is always callable with more specific parameter types.
On the other hand, type parameters in the return type are generally covariant, since a function returning
Baseis not generally usable where you need a function returningDerived, but the opposite is true: a function returningDerivedcan always be used where you need a function that returnsBase.Therefore, when you changed your signature to
def do_work(message: G) -> G:, now you haveGas both a parameter and a return type, so it should be both covariant and contravariant, but it also can't be either one; hence the default of invariant is correct.