Repository: python/mypy

Issue:
Reject bound covariant type variable in an argument type
We should reject a bound covariant type variable in an argument type (i.e., in a contravariant position). For example, this should be rejected:

```
T = TypeVar('T', covariant=True)
class A(Generic[T]):
    def foo(self, x: T) -> None: ...  # Can't use as argument type! Return type is okay.
```

Similarly, a contravariant type variable should not be used in a return type.

The actual rule is probably a bit more involved than what I gave above, but this would probably cover most cases.



Discussion:

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -19,13 +19,14 @@
     LITERAL_TYPE, BreakStmt, ContinueStmt, ComparisonExpr, StarExpr,
     YieldFromExpr, YieldFromStmt, NamedTupleExpr, SetComprehension,
     DictionaryComprehension, ComplexExpr, EllipsisExpr, TypeAliasExpr,
-    RefExpr, YieldExpr
+    RefExpr, YieldExpr, CONTRAVARIANT, COVARIANT
 )
 from mypy.nodes import function_type, method_type, method_type_with_fallback
 from mypy import nodes
 from mypy.types import (
     Type, AnyType, CallableType, Void, FunctionLike, Overloaded, TupleType,
-    Instance, NoneTyp, UnboundType, ErrorType, TypeTranslator, strip_type, UnionType
+    Instance, NoneTyp, UnboundType, ErrorType, TypeTranslator, strip_type,
+    UnionType, TypeVarType,
 )
 from mypy.sametypes import is_same_type
 from mypy.messages import MessageBuilder
@@ -504,12 +505,25 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: str) -> None:
             elif name == '__getattr__':
                 self.check_getattr_method(typ, defn)
 
+            # Refuse contravariant return type variable
+            if isinstance(typ.ret_type, TypeVarType):
+                if typ.ret_type.variance == CONTRAVARIANT:
+                    self.fail(messages.RETURN_TYPE_CANNOT_BE_CONTRAVARIANT,
+                         typ.ret_type)
+
             # Push return type.
             self.return_types.append(typ.ret_type)
 
             # Store argument types.
             for i in range(len(typ.arg_types)):
                 arg_type = typ.arg_types[i]
+
+                # Refuse covariant parameter type variables
+                if isinstance(arg_type, TypeVarType):
+                    if arg_type.variance == COVARIANT:
+                        self.fail(messages.FUNCTION_PARAMETER_CANNOT_BE_COVARIANT,
+                                  arg_type)
+
                 if typ.arg_kinds[i] == nodes.ARG_STAR:
                     # builtins.tuple[T] is typing.Tuple[T, ...]
                     arg_type = self.named_generic_type('builtins.tuple',
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -66,6 +66,8 @@
 FORMAT_REQUIRES_MAPPING = 'Format requires a mapping'
 GENERIC_TYPE_NOT_VALID_AS_EXPRESSION = \
     "Generic type not valid as an expression any more (use '# type:' comment instead)"
+RETURN_TYPE_CANNOT_BE_CONTRAVARIANT = "Cannot use a contravariant type variable as return type"
+FUNCTION_PARAMETER_CANNOT_BE_COVARIANT = "Cannot use a covariant type variable as a parameter"
 
 
 class MessageBuilder:
diff --git a/mypy/typeanal.py b/mypy/typeanal.py
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -77,9 +77,10 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
                 if len(t.args) > 0:
                     self.fail('Type variable "{}" used with arguments'.format(
                         t.name), t)
-                values = cast(TypeVarExpr, sym.node).values
-                return TypeVarType(t.name, sym.tvar_id, values,
+                tvar_expr = cast(TypeVarExpr, sym.node)
+                return TypeVarType(t.name, sym.tvar_id, tvar_expr.values,
                                    self.builtin_type('builtins.object'),
+                                   tvar_expr.variance,
                                    t.line)
             elif fullname == 'builtins.None':
                 return Void()


Test patch:
diff --git a/mypy/test/data/check-functions.test b/mypy/test/data/check-functions.test
--- a/mypy/test/data/check-functions.test
+++ b/mypy/test/data/check-functions.test
@@ -902,3 +902,43 @@ def f():
     """
     return 1
 f() + ''
+[case testRejectCovariantArgument]
+from typing import TypeVar, Generic
+
+t = TypeVar('t', covariant=True)
+class A(Generic[t]):
+    def foo(self, x: t) -> None:
+        return None
+[builtins fixtures/bool.py]
+[out]
+main: note: In member "foo" of class "A":
+main:5: error: Cannot use a covariant type variable as a parameter
+
+[case testRejectContravariantReturnType]
+from typing import TypeVar, Generic
+
+t = TypeVar('t', contravariant=True)
+class A(Generic[t]):
+    def foo(self) -> t:
+        return None
+[builtins fixtures/bool.py]
+[out]
+main: note: In member "foo" of class "A":
+main:5: error: Cannot use a contravariant type variable as return type
+
+[case testAcceptCovariantReturnType]
+from typing import TypeVar, Generic
+
+t = TypeVar('t', covariant=True)
+class A(Generic[t]):
+    def foo(self) -> t:
+        return None
+[builtins fixtures/bool.py]
+[case testAcceptContravariantArgument]
+from typing import TypeVar, Generic
+
+t = TypeVar('t', contravariant=True)
+class A(Generic[t]):
+    def foo(self, x: t) -> None:
+        return None
+[builtins fixtures/bool.py]