Repository: python/mypy
Pull request: Include a note when user forgets to import something from the typing module
Fixes #4317 

Problem statement:
Add import hints for errors like 'Any' undefined
If new users forget to import `Any`, `List` or similar, it could be useful to add a hint such as this:

```
x.py:20: error: Name "Any" undefined
x.py:20: note: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")
```

We could do this for some common stdlib functionality as well, such as `collections`, `subprocess`, `re`, etc.

Even better, mypy could also give import hints for some user-defined classes and functions (that have unique definitions).

Hints from issue discussion:
Maybe also if they misspell it as `any`.
I'm taking a look at this.
The top five imported names are:
* `Optional`
* `Tuple`
* `Union`
* `Callable`
* `Any`

Commit message: Include note error for common unimported modules

Patch to review:
diff --git a/mypy/semanal.py b/mypy/semanal.py
index 72a7c9320972..a00921681049 100644
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -166,6 +166,23 @@
     'builtins.classmethod': 'classmethod.pyi',
 }  # type: Final
 
+# Map from the lowercased name of a missing definition to a tuple containing
+# the name of the likely module definition and a suggested import string.
+TYPES_FOR_UNIMPORTED_HINTS = {
+    'any': ('typing', 'from typing import Any'),
+    'callable': ('typing', 'from typing import Callable'),
+    'dict': ('typing', 'from typing import Dict'),
+    'iterable': ('typing', 'from typing import Iterable'),
+    'iterator': ('typing', 'from typing import Iterator'),
+    'list': ('typing', 'from typing import List'),
+    'optional': ('typing', 'from typing import Optional'),
+    'set': ('typing', 'from typing import Set'),
+    'tuple': ('typing', 'from typing import Tuple'),
+    'typevar': ('typing', 'from typing import TypeVar'),
+    'union': ('typing', 'from typing import Union'),
+    'cast': ('typing', 'from typing import cast'),
+}  # type: Final
+
 
 class SemanticAnalyzerPass2(NodeVisitor[None],
                             SemanticAnalyzerInterface,
@@ -3734,6 +3751,7 @@ def name_not_defined(self, name: str, ctx: Context) -> None:
         if extra:
             message += ' {}'.format(extra)
         self.fail(message, ctx)
+
         if 'builtins.{}'.format(name) in SUGGESTED_TEST_FIXTURES:
             # The user probably has a missing definition in a test fixture. Let's verify.
             fullname = 'builtins.{}'.format(name)
@@ -3741,6 +3759,15 @@ def name_not_defined(self, name: str, ctx: Context) -> None:
                 # Yes. Generate a helpful note.
                 self.add_fixture_note(fullname, ctx)
 
+        if name.lower() in TYPES_FOR_UNIMPORTED_HINTS:
+            # User probably forgot to import these types.
+            unimported_module, suggestion = TYPES_FOR_UNIMPORTED_HINTS[name.lower()]
+            hint = (
+                f'Did you forget to import it from "{unimported_module}"?'
+                f' (Suggestion: "{suggestion}")'
+            )
+            self.note(hint, ctx)
+
     def already_defined(self, name: str, ctx: Context,
                         original_ctx: Optional[Union[SymbolTableNode, SymbolNode]] = None, *,
                         noun: str) -> None:
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
index 001abb546d58..e8a9b0ee8b39 100644
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -2206,3 +2206,21 @@ C in exp  # E: Non-overlapping container check (element type: "Type[C]", contain
 o in exp
 [builtins fixtures/list.pyi]
 [typing fixtures/typing-full.pyi]
+
+
+[case testUnimportedHintAny]
+def f(x: Any) -> None:  # E: Name 'Any' is not defined \
+                        # N: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")
+    pass
+
+
+[case testUnimportedHintAnyLower]
+def f(x: any) -> None:  # E: Name 'any' is not defined \
+                        # N: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")
+    pass
+
+
+[case testUnimportedHintOptional]
+def f(x: Optional[str]) -> None:  # E: Name 'Optional' is not defined \
+                                  # N: Did you forget to import it from "typing"? (Suggestion: "from typing import Optional")
+    pass


Code review comments:
diff_hunk: @@ -3734,13 +3751,23 @@ def name_not_defined(self, name: str, ctx: Context) -> None:
         if extra:
             message += ' {}'.format(extra)
         self.fail(message, ctx)
+
         if 'builtins.{}'.format(name) in SUGGESTED_TEST_FIXTURES:
             # The user probably has a missing definition in a test fixture. Let's verify.
             fullname = 'builtins.{}'.format(name)
             if self.lookup_fully_qualified_or_none(fullname) is None:
                 # Yes. Generate a helpful note.
                 self.add_fixture_note(fullname, ctx)
 
+        if name.lower() in TYPES_FOR_UNIMPORTED_HINTS:
+            # User probably forgot to import these types.
+            unimported_module, suggestion = TYPES_FOR_UNIMPORTED_HINTS[name.lower()]
+            hint = (
+                f'Did you forget to import it from "{unimported_module}"?'
path: mypy/semanal.py
text: @user1:
Unfortunately we can't use f-strings since mypy still supports Python 3.5.

@author:
:+1:

diff_hunk: @@ -166,6 +166,23 @@
     'builtins.classmethod': 'classmethod.pyi',
 }  # type: Final
 
+# Map from the lowercased name of a missing definition to a tuple containing
+# the name of the likely module definition and a suggested import string.
+TYPES_FOR_UNIMPORTED_HINTS = {
+    'any': ('typing', 'from typing import Any'),
+    'callable': ('typing', 'from typing import Callable'),
+    'dict': ('typing', 'from typing import Dict'),
+    'iterable': ('typing', 'from typing import Iterable'),
+    'iterator': ('typing', 'from typing import Iterator'),
+    'list': ('typing', 'from typing import List'),
+    'optional': ('typing', 'from typing import Optional'),
+    'set': ('typing', 'from typing import Set'),
+    'tuple': ('typing', 'from typing import Tuple'),
+    'typevar': ('typing', 'from typing import TypeVar'),
+    'union': ('typing', 'from typing import Union'),
+    'cast': ('typing', 'from typing import cast'),
path: mypy/semanal.py
text: @user1:
Since all of these follow the same pattern, we could perhaps avoid some redundancy. One idea would be to just have a list/set of full names (`typing.Any`, `typing.Callable`, ...) and we can derive the other information (lower case version, `from typing import Foo`) from them.

@author:
:+1: