Repository: python/mypy
Pull request: Improve message when an annotation is expected
Fixes #2463

Improve error message when partial types are found for built-in types `Dict`, `List`, `Set`, `FrozenSet`.

Messages used to be like:
```
main:3: error: Need type annotation for 'x'
```
This PR modifies it to look like:
```
main:3: error: Need type annotation for 'x' (hint: "x: List[<type>] = ...")
```

In case the variable is not of a built-in type the hints are not shown.

Problem statement:
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.

Hints from issue 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.

Commit message: Improve message when an annotation is expected

Patch to review:
diff --git a/mypy/checker.py b/mypy/checker.py
index 6390b381d918..573e38ddb956 100644
--- 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,7 @@ 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
index 87f1cd7e0917..1b9d98a78aeb 100644
--- 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,21 @@ 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: Tuple[int, int]) -> None:
+        hint = ''  # Only gives hint if it's a variable declaration and the partial type is a builtin type
+        if (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 < PYTHON3_VERSION:
+                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/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
index 8fea7ec269ed..32be2830908a 100644
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -1,4 +1,5 @@
 """Plugin for supporting the attrs library (http://www.attrs.org)"""
+import sys
 from collections import OrderedDict
 from typing import Optional, Dict, List, cast, Tuple, Iterable
 
@@ -133,7 +134,8 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
                 # assignment, which is where you would fix the issue.
                 node = self.info[self.name].node
                 assert node is not None
-                ctx.api.msg.need_annotation_for_var(node, self.context)
+                python_version: Tuple[int, int] = sys.version_info[:2]
+                ctx.api.msg.need_annotation_for_var(node, self.context, python_version)
 
             # Convert type not set to Any.
             init_type = AnyType(TypeOfAny.unannotated)
@@ -403,7 +405,8 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext',
     if auto_attribs and not stmt.new_syntax:
         # auto_attribs requires an annotation on *every* attr.ib.
         assert lhs.node is not None
-        ctx.api.msg.need_annotation_for_var(lhs.node, stmt)
+        python_version: Tuple[int, int] = sys.version_info[:2]
+        ctx.api.msg.need_annotation_for_var(lhs.node, stmt, python_version)
         return None
 
     if len(stmt.lvalues) > 1:
diff --git a/mypy/test/data.py b/mypy/test/data.py
index 2c6780fdd26e..165edb6bccd6 100644
--- 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/runtests.py b/runtests.py
index 36c2dfa2a014..ade078fe60c3 100755
--- 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
diff --git a/test-data/unit/README.md b/test-data/unit/README.md
index 3c95a3e0f67e..7454126fe570 100644
--- a/test-data/unit/README.md
+++ b/test-data/unit/README.md
@@ -30,6 +30,8 @@ Add the test in this format anywhere in the file:
 with text "abc..."
 - note a space after `E:` and `flags:`
 - `# E:12` adds column number to the expected error
+- use `\` to escape the `#` character and indicate that the rest of the line is part of 
+the error message
 - repeating `# E: ` several times in one line indicates multiple expected errors in one line
 - `W: ...` and `N: ...` works exactly like `E:`, but report a warning and a note respectively
 - lines that don't contain the above should cause no type check errors
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
index f40373c30211..4c0c5fa2651a 100644
--- 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
index e8a9b0ee8b39..8d50a04fe7da 100644
--- 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
index c3d6c08fe6c5..11c7376de452 100644
--- 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
index d075d439cce6..af3abab0ce23 100644
--- 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
index e7d59525c5bc..656c1b67f5ad 100644
--- 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
index f3bc0481ead2..fcec1f1d78cc 100644
--- 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
index c0dace5fe2b6..8246372a58fe 100644
--- 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/fine-grained.test b/test-data/unit/fine-grained.test
index 5ca900c84a21..78622142da44 100644
--- 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


Code review comments:
diff_hunk: @@ -133,7 +134,8 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
                 # assignment, which is where you would fix the issue.
                 node = self.info[self.name].node
                 assert node is not None
-                ctx.api.msg.need_annotation_for_var(node, self.context)
+                python_version: Tuple[int, int] = sys.version_info[:2]
path: mypy/plugins/attrs.py
text: @user1:
This is slightly questionable, since the target Python version could be different from the current Python version we are running on. If we can't easily get the target Python version, I'd prefer to either not provide a Python version at all (such as giving `None` as the version -- we'd omit the hint in this case), or to provide a constant Python version for consistency (say, `(3, 7)`).

diff_hunk: @@ -1077,8 +1078,21 @@ 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: Tuple[int, int]) -> None:
+        hint = ''  # Only gives hint if it's a variable declaration and the partial type is a builtin type
+        if (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 < PYTHON3_VERSION:
path: mypy/messages.py
text: @user1:
I'd prefer `... < (3, 6)` here since `PYTHON3_VERSION` might get changed to `(3, 7)` in the future and the would then become incorrect.