pythongenericspython-typingcovariance

Why are type-checkers fine with covariant method parameters when they are in a union?


Method parameters should be contravariant, hence defining a covariant generic should raise a type error. However when using a covariant generic in a union pyright, mypy and pyre-check all do not report an error on the following code:

from typing import TypeVar, Generic, Any

T_co = TypeVar("T_co", covariant=True)

class Foo(Generic[T_co]):

    def a(self, arg: T_co)  -> Any:  # Error, like expected as T_co is covariant.
        ...

    def b(self, arg: int | T_co)  -> Any:  # No error, expected one
        ...

As all these type-checkers do not raise an error I wonder is this actually fine, shouldn't this also break type-safety? If it is fine can you explain to why, what differs from a pure covariant implementation that is definitely not safe, shouldn't I be able to break it as well? Or if its not safe, is there an explanation why all type-checkers have the same gap here?


Solution

  • It appears to be a missing feature.


    In Mypy's case, there's a comment about this:

    elif isinstance(arg_type, TypeVarType):
        # Refuse covariant parameter type variables
        # TODO: check recursively for inner type variables
        if (
            arg_type.variance == COVARIANT
            and defn.name not in ("__init__", "__new__", "__post_init__")
            and not is_private(defn.name)  # private methods are not inherited
        ):
            ...
            self.fail(message_registry.FUNCTION_PARAMETER_CANNOT_BE_COVARIANT, ctx)
    

    It was added in 2016 and has been there ever since.


    Pyright has a similar check, which originally prevented invalid nested types as well:

    if (this._containsCovariantTypeVar(paramType, node.id, diag)) {
        this._evaluator.addDiagnostic(
            this._fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
            // ...
        );
    }
    

    This check was then changed a month later, to:

    if (isTypeVar(paramType) && paramType.details.isCovariant && !paramType.details.isSynthesized) {
        this._evaluator.addDiagnostic(
            this._fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
            // ...
        );
    }
    

    The commit message specifically says that such unions would be allowed, but does not give a reason:

    Made the check less strict for the use of covariant type vars within a function input parameter annotation. In particular, unions that contain covariant type vars are now permitted.