Repository: python/mypy

Issue:
Don't report errors inside unannotated functions
When typechecking the following file, we give an error:

```
$ cat /tmp/foo.py
def f(foo):
  if foo is not None:
    dumps = foo.dumps
  else:
    from json import dumps

$ mypy /tmp/foo.py
/tmp/foo.py: note: In function "f":
/tmp/foo.py:5: error: Name 'dumps' already defined
```

Because the function `f` doesn't have a type annotation, we ought to let this code be without complaining about it -- that makes it easier to start using mypy in parts of a large file, and pay the cost of fixing up code like this to work with mypy incrementally.

(Thanks to @varenc for reporting this issue -- he tried putting an annotation on one new function in a 3kLOC file with 188 existing functions, and it had this one error which stopped him.)

Don't report errors inside unannotated functions
When typechecking the following file, we give an error:

```
$ cat /tmp/foo.py
def f(foo):
  if foo is not None:
    dumps = foo.dumps
  else:
    from json import dumps

$ mypy /tmp/foo.py
/tmp/foo.py: note: In function "f":
/tmp/foo.py:5: error: Name 'dumps' already defined
```

Because the function `f` doesn't have a type annotation, we ought to let this code be without complaining about it -- that makes it easier to start using mypy in parts of a large file, and pay the cost of fixing up code like this to work with mypy incrementally.

(Thanks to @varenc for reporting this issue -- he tried putting an annotation on one new function in a 3kLOC file with 188 existing functions, and it had this one error which stopped him.)



Discussion:
I think we should fix this issue not by fixing this particular case but by entirely not type checking unannotated functions.  This would also speed up mypy on codebases that still have a significant fraction of functions unannotated.

I think there may be some cases (related to classes?) where we derive information from unannotated functions (maybe just `__init__`?).  My inclination would be to address these cases by _not_ gathering that information, if possible (e.g. resulting in classes with unannotated `__init__` methods getting `Any` typed).  @JukkaL, do you know when we currently do this?

I'm just speculating, but I wonder if this particular error is happening due to the slightly ad-hoc way we try to deal with conditional imports. The general rule is indeed that we don't type-check code inside un-annotated functions, but this particular error may come out of the semantic analysis, which always runs (or which may be triggered by the presence of the import in one of the branches).

Debugging tip: if you want to know where an error is generated, put a debug breakpoint (e.g. `import pdb; pdb.set_trace()`) in the code that generates the error (in this case `name_already_defined()` in semanal.py) and go up the stack a few frames to find out where it's being called and why. I've learned a ton this way.

This error is coming from semantic analysis, so skipping type checking wouldn't help here. We can probably not run type checking for unannotated functions if we move some things to an earlier phase. The reason we run type checking for all functions is actually so that mypy can export better type information for all AST nodes. These types were used by a compiler prototype that I wrote back in the day, but the information isn't very useful any more, though future features like IDE integration might still benefit from it.

Some behavior might change if we didn't run type checking for unannotated code. Nested annotated functions / classes wouldn't get type checked. We could perhaps special case these, but this seems like a pretty marginal use case. Also, not sure how attribute definitions would behave. In any case, we could infer attribute types during semantic analysis in unannotated functions, as the type would always be `Any`. We could also tweak semantic analysis to not complain about certain things such as name already defined in an unannotated function. 

The best way to fix the original problem would be to support the idiom both in annotated and unannotated functions.

I like the idea of avoiding unannotated functions entirely -- our codebase has a number of errors showing up in unannotated functions. (For example, issue #984)

What you wrote seems ambiguous. Did you mean `--disallow-untyped-defs`? Or
did you mean you don't want any errors to be reported for unannotated
functions?

I was liking @ddfisher's suggestion of not reporting errors for unannotated functions. It would get us much closer to having mypy running without errors on our codebase.

David and I discussed this a bit and we decided that (without looking at the code) the most reasonable approach is to keep doing the semantic analysis for unannotated functions, but to somehow suppress errors from it.

I think we should fix this issue not by fixing this particular case but by entirely not type checking unannotated functions.  This would also speed up mypy on codebases that still have a significant fraction of functions unannotated.

I think there may be some cases (related to classes?) where we derive information from unannotated functions (maybe just `__init__`?).  My inclination would be to address these cases by _not_ gathering that information, if possible (e.g. resulting in classes with unannotated `__init__` methods getting `Any` typed).  @JukkaL, do you know when we currently do this?

I'm just speculating, but I wonder if this particular error is happening due to the slightly ad-hoc way we try to deal with conditional imports. The general rule is indeed that we don't type-check code inside un-annotated functions, but this particular error may come out of the semantic analysis, which always runs (or which may be triggered by the presence of the import in one of the branches).

Debugging tip: if you want to know where an error is generated, put a debug breakpoint (e.g. `import pdb; pdb.set_trace()`) in the code that generates the error (in this case `name_already_defined()` in semanal.py) and go up the stack a few frames to find out where it's being called and why. I've learned a ton this way.

This error is coming from semantic analysis, so skipping type checking wouldn't help here. We can probably not run type checking for unannotated functions if we move some things to an earlier phase. The reason we run type checking for all functions is actually so that mypy can export better type information for all AST nodes. These types were used by a compiler prototype that I wrote back in the day, but the information isn't very useful any more, though future features like IDE integration might still benefit from it.

Some behavior might change if we didn't run type checking for unannotated code. Nested annotated functions / classes wouldn't get type checked. We could perhaps special case these, but this seems like a pretty marginal use case. Also, not sure how attribute definitions would behave. In any case, we could infer attribute types during semantic analysis in unannotated functions, as the type would always be `Any`. We could also tweak semantic analysis to not complain about certain things such as name already defined in an unannotated function. 

The best way to fix the original problem would be to support the idiom both in annotated and unannotated functions.

I like the idea of avoiding unannotated functions entirely -- our codebase has a number of errors showing up in unannotated functions. (For example, issue #984)

What you wrote seems ambiguous. Did you mean `--disallow-untyped-defs`? Or
did you mean you don't want any errors to be reported for unannotated
functions?

I was liking @ddfisher's suggestion of not reporting errors for unannotated functions. It would get us much closer to having mypy running without errors on our codebase.

David and I discussed this a bit and we decided that (without looking at the code) the most reasonable approach is to keep doing the semantic analysis for unannotated functions, but to somehow suppress errors from it.


Resolving patch:
diff --git a/mypy/build.py b/mypy/build.py
--- a/mypy/build.py
+++ b/mypy/build.py
@@ -368,8 +368,10 @@ def __init__(self, data_dir: str,
         self.custom_typing_module = custom_typing_module
         self.source_set = source_set
         self.reports = reports
+        check_untyped_defs = CHECK_UNTYPED_DEFS in self.flags
         self.semantic_analyzer = SemanticAnalyzer(lib_path, self.errors,
-                                                  pyversion=pyversion)
+                                                  pyversion=pyversion,
+                                                  check_untyped_defs=check_untyped_defs)
         self.modules = self.semantic_analyzer.modules
         self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
         self.type_checker = TypeChecker(self.errors,
@@ -377,7 +379,7 @@ def __init__(self, data_dir: str,
                                         self.pyversion,
                                         DISALLOW_UNTYPED_CALLS in self.flags,
                                         DISALLOW_UNTYPED_DEFS in self.flags,
-                                        CHECK_UNTYPED_DEFS in self.flags)
+                                        check_untyped_defs)
         self.missing_modules = set()  # type: Set[str]
 
     def all_imported_modules_in_file(self,
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -190,8 +190,11 @@ class SemanticAnalyzer(NodeVisitor):
     imports = None  # type: Set[str]  # Imported modules (during phase 2 analysis)
     errors = None  # type: Errors     # Keeps track of generated errors
 
-    def __init__(self, lib_path: List[str], errors: Errors,
-                 pyversion: Tuple[int, int] = defaults.PYTHON3_VERSION) -> None:
+    def __init__(self,
+                 lib_path: List[str],
+                 errors: Errors,
+                 pyversion: Tuple[int, int],
+                 check_untyped_defs: bool) -> None:
         """Construct semantic analyzer.
 
         Use lib_path to search for modules, and report analysis errors
@@ -211,6 +214,7 @@ def __init__(self, lib_path: List[str], errors: Errors,
         self.errors = errors
         self.modules = {}
         self.pyversion = pyversion
+        self.check_untyped_defs = check_untyped_defs
         self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES]
         self.postponed_functions_stack = []
 
@@ -244,10 +248,12 @@ def visit_file(self, file_node: MypyFile, fnam: str) -> None:
     def visit_func_def(self, defn: FuncDef) -> None:
         phase_info = self.postpone_nested_functions_stack[-1]
         if phase_info != FUNCTION_SECOND_PHASE:
+            self.function_stack.append(defn)
             # First phase of analysis for function.
             self.errors.push_function(defn.name())
             self.update_function_type_variables(defn)
             self.errors.pop_function()
+            self.function_stack.pop()
 
             defn.is_conditional = self.block_depth[-1] > 0
 
@@ -1630,11 +1636,11 @@ def visit_for_stmt(self, s: ForStmt) -> None:
 
     def visit_break_stmt(self, s: BreakStmt) -> None:
         if self.loop_depth == 0:
-            self.fail("'break' outside loop", s)
+            self.fail("'break' outside loop", s, True)
 
     def visit_continue_stmt(self, s: ContinueStmt) -> None:
         if self.loop_depth == 0:
-            self.fail("'continue' outside loop", s)
+            self.fail("'continue' outside loop", s, True)
 
     def visit_if_stmt(self, s: IfStmt) -> None:
         infer_reachability_of_if_statement(s, pyversion=self.pyversion)
@@ -2203,10 +2209,19 @@ def name_not_defined(self, name: str, ctx: Context) -> None:
     def name_already_defined(self, name: str, ctx: Context) -> None:
         self.fail("Name '{}' already defined".format(name), ctx)
 
-    def fail(self, msg: str, ctx: Context) -> None:
+    def fail(self, msg: str, ctx: Context, serious: bool = False) -> None:
+        if (not serious and
+                not self.check_untyped_defs and
+                self.function_stack and
+                self.function_stack[-1].is_dynamic()):
+            return
         self.errors.report(ctx.get_line(), msg)
 
     def note(self, msg: str, ctx: Context) -> None:
+        if (not self.check_untyped_defs and
+                self.function_stack and
+                self.function_stack[-1].is_dynamic()):
+            return
         self.errors.report(ctx.get_line(), msg, severity='note')
 
     def undefined_name_extra_info(self, fullname: str) -> Optional[str]:


Test patch:
diff --git a/mypy/test/data/check-weak-typing.test b/mypy/test/data/check-weak-typing.test
--- a/mypy/test/data/check-weak-typing.test
+++ b/mypy/test/data/check-weak-typing.test
@@ -123,14 +123,6 @@ def f():
     1 + 'a' # E: Unsupported left operand type for + ("int")
 [out]
 main: note: In function "f":
-[case testNonWeakFunction]
-def f():
-    if y:      # E: Name 'y' is not defined
-        x = 1
-    else:
-        x = 'a'
-[out]
-main: note: In function "f":
 [case testWeakFunctionCall]
 # mypy: weak=global
 def f(a: str) -> None: pass
diff --git a/mypy/test/data/semanal-errors.test b/mypy/test/data/semanal-errors.test
--- a/mypy/test/data/semanal-errors.test
+++ b/mypy/test/data/semanal-errors.test
@@ -17,7 +17,7 @@ main:3: error: Name 'y' is not defined
 
 [case testUndefinedVariableWithinFunctionContext]
 import typing
-def f():
+def f() -> None:
   x
 y
 [out]
@@ -39,7 +39,7 @@ import typing
 class A:
   def f(self): pass
 class B:
-  def g(self):
+  def g(self) -> None:
     f # error
     g # error
 [out]
@@ -178,7 +178,7 @@ main:4: error: Name 'x' already defined
 [case testLocalVarRedefinition]
 import typing
 class A: pass
-def f():
+def f() -> None:
   x = 0 # type: A
   x = 0 # type: A
 [out]
@@ -523,7 +523,7 @@ main:4: error: Invalid type "__main__.t"
 from typing import TypeVar, Generic
 t = TypeVar('t')
 class c(Generic[t]):
-    def f(self): x = t
+    def f(self) -> None: x = t
 def f(y: t): x = t
 [out]
 main: note: In function "f":
@@ -546,7 +546,7 @@ main:2: error: Name 'B' is not defined
 [case testSuperOutsideClass]
 class A: pass
 super().x
-def f(): super().y
+def f() -> None: super().y
 [out]
 main:2: error: "super" used outside class
 main: note: In function "f":
@@ -572,7 +572,7 @@ main:5: error: Name 'f' already defined
 
 [case testInvalidGlobalDecl]
 import typing
-def f():
+def f() -> None:
     global x
     x = None
 [out]
@@ -582,7 +582,7 @@ main:4: error: Name 'x' is not defined
 [case testInvalidNonlocalDecl]
 import typing
 def f():
-    def g():
+    def g() -> None:
        nonlocal x
        x = None
 [out]
@@ -593,7 +593,7 @@ main:5: error: Name 'x' is not defined
 [case testNonlocalDeclNotMatchingGlobal]
 import typing
 x = None
-def f():
+def f() -> None:
     nonlocal x
     x = None
 [out]
@@ -605,7 +605,7 @@ main:5: error: Name 'x' is not defined
 import typing
 def g():
     x = None
-    def f(x):
+    def f(x) -> None:
         nonlocal x
         x = None
 [out]
@@ -623,7 +623,7 @@ import typing
 x = 1
 def f():
     x = 1
-    def g():
+    def g() -> None:
        global x
        nonlocal x
        x = None
@@ -636,7 +636,7 @@ import typing
 x = 1
 def f():
     x = 1
-    def g():
+    def g() -> None:
        nonlocal x
        global x
        x = None
@@ -646,7 +646,7 @@ main:7: error: Name 'x' is nonlocal and global
 
 [case testNestedFunctionAndScoping]
 import typing
-def f(x):
+def f(x) -> None:
     def g(y):
         z = x
     z
@@ -659,7 +659,7 @@ main:6: error: Name 'y' is not defined
 
 [case testMultipleNestedFunctionDef]
 import typing
-def f(x):
+def f(x) -> None:
     def g(): pass
     x = 1
     def g(): pass
@@ -669,7 +669,7 @@ main:5: error: Name 'g' already defined
 
 [case testRedefinedOverloadedFunction]
 from typing import overload, Any
-def f():
+def f() -> None:
     @overload
     def p(o: object) -> None: pass # no error
     @overload
@@ -683,8 +683,8 @@ main:8: error: Name 'p' already defined
 [case testNestedFunctionInMethod]
 import typing
 class A:
-   def f(self):
-       def g():
+   def f(self) -> None:
+       def g() -> None:
            x
        y
 [out]
@@ -861,7 +861,7 @@ main:3: error: 'abstractmethod' used with a non-method
 [case testAbstractNestedFunction]
 import typing
 from abc import abstractmethod
-def g():
+def g() -> None:
   @abstractmethod
   def foo(): pass
 [out]
@@ -1133,7 +1133,7 @@ import typing
 @staticmethod
 def f(): pass
 class A:
-  def g(self):
+  def g(self) -> None:
     @staticmethod
     def h(): pass
 [builtins fixtures/staticmethod.py]
@@ -1147,7 +1147,7 @@ import typing
 @classmethod
 def f(): pass
 class A:
-  def g(self):
+  def g(self) -> None:
     @classmethod
     def h(): pass
 [builtins fixtures/classmethod.py]
diff --git a/mypy/test/data/semanal-statements.test b/mypy/test/data/semanal-statements.test
--- a/mypy/test/data/semanal-statements.test
+++ b/mypy/test/data/semanal-statements.test
@@ -534,7 +534,7 @@ MypyFile:1(
             IntExpr(0)))))))
 
 [case testDelMultipleThingsInvalid]
-def f(x, y):
+def f(x, y) -> None:
     del x, y + 1
 [out]
 main: note: In function "f":