Repository: python/mypy

Issue:
Improve message when an annotation is expected
In cases like this, the current error message isn't as helpful as it could be:

```
x = Queue()  # Need type annotation for variable
```

Here's an idea for a better message:

```
x = Queue()  # Need type annotation for variable (hint: # type: Queue[<type>])
x = []  # Need type annotation for variable (hint: # type: List[<type>])
```

We could recognize the common case where the initializer constructs a generic instance and there is not enough information to infer all the type arguments. For other cases we can fall back to the current message.

The idea came from @aleksanb on gitter.


Discussion:
If the type checking target is 3.6+ we should propose a non-comment annotation. Suggestion:

```py
x = Queue()  # Need type annotation for variable (hint: "x: Queue[<type>] = ...")
x = []  # Need type annotation for variable (hint: "x: List[<type>] = ...")
```

Another idea is to add a note about why this is needed or a pointer to documentation:

```py
a.py:3: error: Need type annotation for variable (hint: "x: Queue[<type>] = ...")
a.py:3: note: See also http://mypy.readthedocs.io/en/latest/common_issues.html#types-of-empty-collections
```

(We might want to use a URL shortener or other ways of supporting shorter URLs in error messages.)
I think adding a link to docs is unnecessarily verbose. It will help solve the problem the first time someone using mypy experiences it, but afterwards it doubles the length of the message without adding information. It would require maintaining all the URLs for docs, too.
I'm working on this one.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -2466,13 +2466,13 @@ def infer_variable_type(self, name: Var, lvalue: Lvalue,
             # partial type which will be made more specific later. A partial type
             # gets generated in assignment like 'x = []' where item type is not known.
             if not self.infer_partial_type(name, lvalue, init_type):
-                self.msg.need_annotation_for_var(name, context)
+                self.msg.need_annotation_for_var(name, context, self.options.python_version)
                 self.set_inference_error_fallback_type(name, lvalue, init_type, context)
         elif (isinstance(lvalue, MemberExpr) and self.inferred_attribute_types is not None
               and lvalue.def_var and lvalue.def_var in self.inferred_attribute_types
               and not is_same_type(self.inferred_attribute_types[lvalue.def_var], init_type)):
             # Multiple, inconsistent types inferred for an attribute.
-            self.msg.need_annotation_for_var(name, context)
+            self.msg.need_annotation_for_var(name, context, self.options.python_version)
             name.type = AnyType(TypeOfAny.from_error)
         else:
             # Infer type of the target.
@@ -3694,7 +3694,7 @@ def enter_partial_types(self, *, is_function: bool = False,
                     var.type = NoneType()
                 else:
                     if var not in self.partial_reported and not permissive:
-                        self.msg.need_annotation_for_var(var, context)
+                        self.msg.need_annotation_for_var(var, context, self.options.python_version)
                         self.partial_reported.add(var)
                     if var.type:
                         var.type = self.fixup_partial_type(var.type)
@@ -3718,7 +3718,8 @@ def handle_partial_var_type(
                 if in_scope:
                     context = partial_types[node]
                     if is_local or not self.options.allow_untyped_globals:
-                        self.msg.need_annotation_for_var(node, context)
+                        self.msg.need_annotation_for_var(node, context,
+                                                         self.options.python_version)
                 else:
                     # Defer the node -- we might get a better type in the outer scope
                     self.handle_cannot_determine_type(node.name(), context)
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -21,7 +21,7 @@
 from mypy.types import (
     Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType, LiteralType,
     UnionType, NoneType, AnyType, Overloaded, FunctionLike, DeletedType, TypeType,
-    UninhabitedType, TypeOfAny, ForwardRef, UnboundType
+    UninhabitedType, TypeOfAny, ForwardRef, UnboundType, PartialType
 )
 from mypy.nodes import (
     TypeInfo, Context, MypyFile, op_methods, FuncDef, reverse_builtin_aliases,
@@ -29,6 +29,7 @@
     ReturnStmt, NameExpr, Var, CONTRAVARIANT, COVARIANT, SymbolNode,
     CallExpr
 )
+from mypy.defaults import PYTHON3_VERSION
 from mypy.util import unmangle
 from mypy import message_registry
 
@@ -1077,8 +1078,22 @@ def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> N
         self.fail("{} becomes {} due to an unfollowed import".format(prefix, self.format(typ)),
                   ctx)
 
-    def need_annotation_for_var(self, node: SymbolNode, context: Context) -> None:
-        self.fail("Need type annotation for '{}'".format(unmangle(node.name())), context)
+    def need_annotation_for_var(self, node: SymbolNode, context: Context,
+                                python_version: Optional[Tuple[int, int]] = None) -> None:
+        hint = ''
+        # Only gives hint if it's a variable declaration and the partial type is a builtin type
+        if (python_version and isinstance(node, Var) and isinstance(node.type, PartialType) and
+                node.type.type and node.type.type.fullname() in reverse_builtin_aliases):
+            alias = reverse_builtin_aliases[node.type.type.fullname()]
+            alias = alias.split('.')[-1]
+            type_dec = '<type>'
+            if alias == 'Dict':
+                type_dec = '{}, {}'.format(type_dec, type_dec)
+            if python_version < (3, 6):
+                hint = ' (hint: "{} = ...  # type: {}[{}]")'.format(node.name(), alias, type_dec)
+            else:
+                hint = ' (hint: "{}: {}[{}] = ...")'.format(node.name(), alias, type_dec)
+        self.fail("Need type annotation for '{}'{}".format(unmangle(node.name()), hint), context)
 
     def explicit_any(self, ctx: Context) -> None:
         self.fail('Explicit "Any" is not allowed', ctx)
diff --git a/runtests.py b/runtests.py
--- a/runtests.py
+++ b/runtests.py
@@ -34,7 +34,7 @@
                 STUBGEN_PY]
 
 # We split the pytest run into three parts to improve test
-# parallelization. Each run should have tests that each take a roughly similiar
+# parallelization. Each run should have tests that each take a roughly similar
 # time to run.
 cmds = {
     # Self type check


Test patch:
diff --git a/mypy/test/data.py b/mypy/test/data.py
--- a/mypy/test/data.py
+++ b/mypy/test/data.py
@@ -513,12 +513,14 @@ def expand_errors(input: List[str], output: List[str], fnam: str) -> None:
                 elif m.group(1) == 'W':
                     severity = 'warning'
                 col = m.group('col')
+                message = m.group('message')
+                message = message.replace('\\#', '#')  # adds back escaped # character
                 if col is None:
                     output.append(
-                        '{}:{}: {}: {}'.format(fnam, i + 1, severity, m.group('message')))
+                        '{}:{}: {}: {}'.format(fnam, i + 1, severity, message))
                 else:
                     output.append('{}:{}:{}: {}: {}'.format(
-                        fnam, i + 1, col, severity, m.group('message')))
+                        fnam, i + 1, col, severity, message))
 
 
 def fix_win_path(line: str) -> str:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -912,7 +912,7 @@ class C:
 x = C.x
 [builtins fixtures/list.pyi]
 [out]
-main:2: error: Need type annotation for 'x'
+main:2: error: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
 
 [case testAccessingGenericClassAttribute]
 from typing import Generic, TypeVar
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -1714,7 +1714,7 @@ d5 = dict(a=1, b='') # type: Dict[str, Any]
 
 [case testDictWithoutKeywordArgs]
 from typing import Dict
-d = dict() # E: Need type annotation for 'd'
+d = dict() # E: Need type annotation for 'd' (hint: "d: Dict[<type>, <type>] = ...")
 d2 = dict() # type: Dict[int, str]
 dict(undefined) # E: Name 'undefined' is not defined
 [builtins fixtures/dict.pyi]
@@ -1953,8 +1953,8 @@ a.__pow__() # E: Too few arguments for "__pow__" of "int"
 [builtins fixtures/ops.pyi]
 
 [case testTypeAnnotationNeededMultipleAssignment]
-x, y = [], [] # E: Need type annotation for 'x' \
-            # E: Need type annotation for 'y'
+x, y = [], [] # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...") \
+            # E: Need type annotation for 'y' (hint: "y: List[<type>] = ...")
 [builtins fixtures/list.pyi]
 
 [case testStrictEqualityEq]
diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test
--- a/test-data/unit/check-functions.test
+++ b/test-data/unit/check-functions.test
@@ -1514,7 +1514,7 @@ if g(C()):
     def f(x: B) -> B: pass
 
 [case testRedefineFunctionDefinedAsVariableInitializedToEmptyList]
-f = [] # E: Need type annotation for 'f'
+f = [] # E: Need type annotation for 'f' (hint: "f: List[<type>] = ...")
 if object():
     def f(): pass # E: Incompatible redefinition
 f()  # E: "List[Any]" not callable
@@ -2320,7 +2320,7 @@ def make_list() -> List[T]: pass
 
 l: List[int] = make_list()
 
-bad = make_list()  # E: Need type annotation for 'bad'
+bad = make_list()  # E: Need type annotation for 'bad' (hint: "bad: List[<type>] = ...")
 [builtins fixtures/list.pyi]
 
 [case testAnonymousArgumentError]
diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test
--- a/test-data/unit/check-inference-context.test
+++ b/test-data/unit/check-inference-context.test
@@ -425,7 +425,7 @@ class B(A): pass
 [case testLocalVariableInferenceFromEmptyList]
 import typing
 def f() -> None:
-    a = []     # E: Need type annotation for 'a'
+    a = []     # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
     b = [None]
     c = [B()]
     if int():
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -1375,12 +1375,12 @@ a.append(0)  # E: Argument 1 to "append" of "list" has incompatible type "int";
 [out]
 
 [case testInferListInitializedToEmptyAndNotAnnotated]
-a = []  # E: Need type annotation for 'a'
+a = []  # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
 [builtins fixtures/list.pyi]
 [out]
 
 [case testInferListInitializedToEmptyAndReadBeforeAppend]
-a = []  # E: Need type annotation for 'a'
+a = []  # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
 if a: pass
 a.xyz  # E: "List[Any]" has no attribute "xyz"
 a.append('')
@@ -1388,7 +1388,7 @@ a.append('')
 [out]
 
 [case testInferListInitializedToEmptyAndIncompleteTypeInAppend]
-a = [] # E: Need type annotation for 'a'
+a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
 a.append([])
 a()  # E: "List[Any]" not callable
 [builtins fixtures/list.pyi]
@@ -1413,7 +1413,7 @@ def f() -> None:
 
 [case testInferListInitializedToEmptyAndNotAnnotatedInFunction]
 def f() -> None:
-    a = []  # E: Need type annotation for 'a'
+    a = []  # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
 
 def g() -> None: pass
 
@@ -1424,7 +1424,7 @@ a.append(1)
 
 [case testInferListInitializedToEmptyAndReadBeforeAppendInFunction]
 def f() -> None:
-    a = []  # E: Need type annotation for 'a'
+    a = []  # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
     if a: pass
     a.xyz  # E: "List[Any]" has no attribute "xyz"
     a.append('')
@@ -1441,7 +1441,7 @@ class A:
 
 [case testInferListInitializedToEmptyAndNotAnnotatedInClassBody]
 class A:
-    a = []  # E: Need type annotation for 'a'
+    a = []  # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
 
 class B:
     a = []
@@ -1461,7 +1461,7 @@ class A:
 [case testInferListInitializedToEmptyAndNotAnnotatedInMethod]
 class A:
     def f(self) -> None:
-        a = []  # E: Need type annotation for 'a'
+        a = []  # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
 [builtins fixtures/list.pyi]
 [out]
 
@@ -1469,7 +1469,7 @@ class A:
 class A:
     def f(self) -> None:
         # Attributes aren't supported right now.
-        self.a = [] # E: Need type annotation for 'a'
+        self.a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
         self.a.append(1)
         self.a.append('')
 [builtins fixtures/list.pyi]
@@ -1480,7 +1480,7 @@ from typing import List
 
 class A:
     def __init__(self) -> None:
-        self.x = [] # E: Need type annotation for 'x'
+        self.x = [] # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
 
 class B(A):
     # TODO?: This error is kind of a false positive, unfortunately
@@ -1526,16 +1526,16 @@ a() # E: "Dict[str, int]" not callable
 [out]
 
 [case testInferDictInitializedToEmptyUsingUpdateError]
-a = {}  # E: Need type annotation for 'a'
+a = {}  # E: Need type annotation for 'a' (hint: "a: Dict[<type>, <type>] = ...")
 a.update([1, 2])  # E: Argument 1 to "update" of "dict" has incompatible type "List[int]"; expected "Mapping[Any, Any]"
 a()  # E: "Dict[Any, Any]" not callable
 [builtins fixtures/dict.pyi]
 [out]
 
 [case testInferDictInitializedToEmptyAndIncompleteTypeInUpdate]
-a = {} # E: Need type annotation for 'a'
+a = {} # E: Need type annotation for 'a' (hint: "a: Dict[<type>, <type>] = ...")
 a[1] = {}
-b = {} # E: Need type annotation for 'b'
+b = {} # E: Need type annotation for 'b' (hint: "b: Dict[<type>, <type>] = ...")
 b[{}] = 1
 [builtins fixtures/dict.pyi]
 [out]
@@ -1555,14 +1555,14 @@ def add():
 [case testSpecialCaseEmptyListInitialization]
 def f(blocks: Any): # E: Name 'Any' is not defined \
                     # N: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")
-    to_process = [] # E: Need type annotation for 'to_process'
+    to_process = [] # E: Need type annotation for 'to_process' (hint: "to_process: List[<type>] = ...")
     to_process = list(blocks)
 [builtins fixtures/list.pyi]
 [out]
 
 [case testSpecialCaseEmptyListInitialization2]
 def f(blocks: object):
-    to_process = [] # E: Need type annotation for 'to_process'
+    to_process = [] # E: Need type annotation for 'to_process' (hint: "to_process: List[<type>] = ...")
     to_process = list(blocks) # E: No overload variant of "list" matches argument type "object" \
                               # N: Possible overload variant: \
                               # N:     def [T] __init__(self, x: Iterable[T]) -> List[T] \
@@ -1621,7 +1621,7 @@ x.append('') # E: Argument 1 to "append" of "list" has incompatible type "str";
 x = None
 if object():
     # Promote from partial None to partial list.
-    x = []  # E: Need type annotation for 'x'
+    x = []  # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
     x
 [builtins fixtures/list.pyi]
 
@@ -1630,7 +1630,7 @@ def f() -> None:
     x = None
     if object():
         # Promote from partial None to partial list.
-        x = []  # E: Need type annotation for 'x'
+        x = []  # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
 [builtins fixtures/list.pyi]
 [out]
 
@@ -1716,7 +1716,7 @@ class A:
             pass
 [builtins fixtures/for.pyi]
 [out]
-main:3: error: Need type annotation for 'x'
+main:3: error: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
 
 [case testPartialTypeErrorSpecialCase3]
 class A:
@@ -1882,9 +1882,9 @@ o = 1
 
 [case testMultipassAndPartialTypesSpecialCase3]
 def f() -> None:
-    x = {} # E: Need type annotation for 'x'
+    x = {} # E: Need type annotation for 'x' (hint: "x: Dict[<type>, <type>] = ...")
     y = o
-    z = {} # E: Need type annotation for 'z'
+    z = {} # E: Need type annotation for 'z' (hint: "z: Dict[<type>, <type>] = ...")
 o = 1
 [builtins fixtures/dict.pyi]
 [out]
@@ -2070,7 +2070,7 @@ main:4: error: Invalid type: try using Literal[0] instead?
 class C:
     x = None
     def __init__(self) -> None:
-        self.x = []  # E: Need type annotation for 'x'
+        self.x = []  # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
 [builtins fixtures/list.pyi]
 [out]
 
@@ -2246,7 +2246,7 @@ class A:
 [case testLocalPartialTypesWithClassAttributeInitializedToEmptyDict]
 # flags: --local-partial-types
 class A:
-    x = {}  # E: Need type annotation for 'x'
+    x = {}  # E: Need type annotation for 'x' (hint: "x: Dict[<type>, <type>] = ...")
 
     def f(self) -> None:
         self.x[0] = ''
@@ -2269,7 +2269,7 @@ reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]'
 
 [case testLocalPartialTypesWithGlobalInitializedToEmptyList2]
 # flags: --local-partial-types
-a = [] # E: Need type annotation for 'a'
+a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
 
 def f() -> None:
     a.append(1)
@@ -2280,7 +2280,7 @@ reveal_type(a) # E: Revealed type is 'builtins.list[Any]'
 
 [case testLocalPartialTypesWithGlobalInitializedToEmptyList3]
 # flags: --local-partial-types
-a = [] # E: Need type annotation for 'a'
+a = [] # E: Need type annotation for 'a' (hint: "a: List[<type>] = ...")
 
 def f():
     a.append(1)
@@ -2302,7 +2302,7 @@ reveal_type(a) # E: Revealed type is 'builtins.dict[builtins.int, builtins.str]'
 
 [case testLocalPartialTypesWithGlobalInitializedToEmptyDict2]
 # flags: --local-partial-types
-a = {} # E: Need type annotation for 'a'
+a = {} # E: Need type annotation for 'a' (hint: "a: Dict[<type>, <type>] = ...")
 
 def f() -> None:
     a[0] = ''
@@ -2313,7 +2313,7 @@ reveal_type(a) # E: Revealed type is 'builtins.dict[Any, Any]'
 
 [case testLocalPartialTypesWithGlobalInitializedToEmptyDict3]
 # flags: --local-partial-types
-a = {} # E: Need type annotation for 'a'
+a = {} # E: Need type annotation for 'a' (hint: "a: Dict[<type>, <type>] = ...")
 
 def f():
     a[0] = ''
diff --git a/test-data/unit/check-python2.test b/test-data/unit/check-python2.test
--- a/test-data/unit/check-python2.test
+++ b/test-data/unit/check-python2.test
@@ -363,3 +363,8 @@ def foo(
 [case testNoneHasNoBoolInPython2]
 none = None
 b = none.__bool__() # E: "None" has no attribute "__bool__"
+
+[case testDictWithoutTypeCommentInPython2]
+# flags: --py2
+d = dict() # E: Need type annotation for 'd' (hint: "d = ...  \# type: Dict[<type>, <type>]")
+[builtins_py2 fixtures/floatdict_python2.pyi]
diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test
--- a/test-data/unit/check-tuples.test
+++ b/test-data/unit/check-tuples.test
@@ -511,8 +511,8 @@ d, e = f, g, h = 1, 1 # E: Need more than 2 values to unpack (3 expected)
 [case testAssignmentToStarMissingAnnotation]
 from typing import List
 t = 1, 2
-a, b, *c = 1, 2  # E: Need type annotation for 'c'
-aa, bb, *cc = t  # E: Need type annotation for 'cc'
+a, b, *c = 1, 2  # E: Need type annotation for 'c' (hint: "c: List[<type>] = ...")
+aa, bb, *cc = t  # E: Need type annotation for 'cc' (hint: "cc: List[<type>] = ...")
 [builtins fixtures/list.pyi]
 
 [case testAssignmentToStarAnnotation]
diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test
--- a/test-data/unit/cmdline.test
+++ b/test-data/unit/cmdline.test
@@ -924,7 +924,7 @@ y: List = []  # error
 m.py:3: error: Missing type parameters for generic type
 m.py:5: error: Missing type parameters for generic type
 m.py:6: error: Missing type parameters for generic type
-m.py:8: error: Need type annotation for 'x'
+m.py:8: error: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
 m.py:9: error: Missing type parameters for generic type
 
 [case testDisallowAnyGenericsCustomGenericClass]
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -2699,9 +2699,9 @@ def g() -> None: pass
 def g() -> int: pass
 [builtins fixtures/dict.pyi]
 [out]
-main:7: error: Need type annotation for 'x'
+main:7: error: Need type annotation for 'x' (hint: "x: Dict[<type>, <type>] = ...")
 ==
-main:7: error: Need type annotation for 'x'
+main:7: error: Need type annotation for 'x' (hint: "x: Dict[<type>, <type>] = ...")
 
 [case testRefreshPartialTypeInClass]
 import a
@@ -2716,9 +2716,9 @@ def g() -> None: pass
 def g() -> int: pass
 [builtins fixtures/dict.pyi]
 [out]
-main:5: error: Need type annotation for 'x'
+main:5: error: Need type annotation for 'x' (hint: "x: Dict[<type>, <type>] = ...")
 ==
-main:5: error: Need type annotation for 'x'
+main:5: error: Need type annotation for 'x' (hint: "x: Dict[<type>, <type>] = ...")
 
 [case testRefreshTryExcept]
 import a