Repository: python/mypy

Issue:
Give better error messages when there are covariance/invariance related errors
[The following is adapted from discussion at #1113. Go there for more context.]

We should give output like this for some invariance violations, as many programmers expect everything to be covariant and it may not be obvious why mypy is complaining:

```
$ mypy file.py
file.py:7: error: Argument 1 to "f" has incompatible type Dict[str, List[int]]; expected Dict[str, Sequence[int]]
file.py:8: note: The second type parameter of "Dict" is invariant; consider using "Mapping" instead, which is covariant
```

This would give more context even for somebody who doesn't understand covariance/invariance -- these terms are easy to google.

Even better, we could give a pointer to relevant documentation.



Discussion:
I will start looking at this.

Resolving patch:
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -533,6 +533,7 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type:
             target = 'to {} '.format(name)
 
         msg = ''
+        notes = []  # type: List[str]
         if callee.name == '<list>':
             name = callee.name[1:-1]
             n -= 1
@@ -571,7 +572,12 @@ def incompatible_argument(self, n: int, m: int, callee: CallableType, arg_type:
                 arg_type_str = '**' + arg_type_str
             msg = 'Argument {} {}has incompatible type {}; expected {}'.format(
                 n, target, arg_type_str, expected_type_str)
+            if isinstance(arg_type, Instance) and isinstance(expected_type, Instance):
+                notes = append_invariance_notes(notes, arg_type, expected_type)
         self.fail(msg, context)
+        if notes:
+            for note_msg in notes:
+                self.note(note_msg, context)
 
     def invalid_index_type(self, index_type: Type, expected_type: Type, base_str: str,
                            context: Context) -> None:
@@ -992,6 +998,33 @@ def pretty_or(args: List[str]) -> str:
     return ", ".join(quoted[:-1]) + ", or " + quoted[-1]
 
 
+def append_invariance_notes(notes: List[str], arg_type: Instance,
+                            expected_type: Instance) -> List[str]:
+    """Explain that the type is invariant and give notes for how to solve the issue."""
+    from mypy.subtypes import is_subtype
+    from mypy.sametypes import is_same_type
+    invariant_type = ''
+    covariant_suggestion = ''
+    if (arg_type.type.fullname() == 'builtins.list' and
+            expected_type.type.fullname() == 'builtins.list' and
+            is_subtype(arg_type.args[0], expected_type.args[0])):
+        invariant_type = 'List'
+        covariant_suggestion = 'Consider using "Sequence" instead, which is covariant'
+    elif (arg_type.type.fullname() == 'builtins.dict' and
+          expected_type.type.fullname() == 'builtins.dict' and
+          is_same_type(arg_type.args[0], expected_type.args[0]) and
+          is_subtype(arg_type.args[1], expected_type.args[1])):
+        invariant_type = 'Dict'
+        covariant_suggestion = ('Consider using "Mapping" instead, '
+                                'which is covariant in the value type')
+    if invariant_type and covariant_suggestion:
+        notes.append(
+            '"{}" is invariant -- see '.format(invariant_type) +
+            'http://mypy.readthedocs.io/en/latest/common_issues.html#variance')
+        notes.append(covariant_suggestion)
+    return notes
+
+
 def make_inferred_type_note(context: Context, subtype: Type,
                             supertype: Type, supertype_str: str) -> str:
     """Explain that the user may have forgotten to type a variable.


Test patch:
diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test
--- a/test-data/unit/check-varargs.test
+++ b/test-data/unit/check-varargs.test
@@ -592,3 +592,48 @@ class C:
     def foo(self) -> None: pass
 C().foo()
 C().foo(1)  # The decorator's return type says this should be okay
+
+[case testInvariantDictArgNote]
+from typing import Dict, Sequence
+def f(x: Dict[str, Sequence[int]]) -> None: pass
+def g(x: Dict[str, float]) -> None: pass
+def h(x: Dict[str, int]) -> None: pass
+a = {'a': [1, 2]}
+b = {'b': ['c', 'd']}
+c = {'c': 1.0}
+d = {'d': 1}
+f(a) # E: Argument 1 to "f" has incompatible type Dict[str, List[int]]; expected Dict[str, Sequence[int]] \
+     # N: "Dict" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
+     # N: Consider using "Mapping" instead, which is covariant in the value type
+f(b) # E: Argument 1 to "f" has incompatible type Dict[str, List[str]]; expected Dict[str, Sequence[int]]
+g(c)
+g(d) # E: Argument 1 to "g" has incompatible type Dict[str, int]; expected Dict[str, float] \
+     # N: "Dict" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
+     # N: Consider using "Mapping" instead, which is covariant in the value type
+h(c) # E: Argument 1 to "h" has incompatible type Dict[str, float]; expected Dict[str, int]
+h(d)
+[builtins fixtures/dict.pyi]
+
+[case testInvariantListArgNote]
+from typing import List, Union
+def f(numbers: List[Union[int, float]]) -> None: pass
+a = [1, 2]
+f(a) # E: Argument 1 to "f" has incompatible type List[int]; expected List[Union[int, float]] \
+     # N: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance \
+     # N: Consider using "Sequence" instead, which is covariant
+x = [1]
+y = ['a']
+x = y # E: Incompatible types in assignment (expression has type List[str], variable has type List[int])
+[builtins fixtures/list.pyi]
+
+[case testInvariantTypeConfusingNames]
+from typing import TypeVar
+class Listener: pass
+class DictReader: pass
+def f(x: Listener) -> None: pass
+def g(y: DictReader) -> None: pass
+a = [1, 2]
+b = {'b': 1}
+f(a) # E: Argument 1 to "f" has incompatible type List[int]; expected "Listener"
+g(b) # E: Argument 1 to "g" has incompatible type Dict[str, int]; expected "DictReader"
+[builtins fixtures/dict.pyi]