Repository: python/mypy

Issue:
Untyped functions content show up in mypy output
**Bug Report**

For default configuration, violations can be reported in untyped (shouldn't be checked) functions.

**To Reproduce**

```
def f():
    return UNDEFINED


def g():
    return lambda x: UNDEFINED in x
```

https://mypy-play.net/?mypy=latest&python=3.10&gist=c49b86bc9067f234a038a71598d2b3b7

**Expected Behavior**

Both `f()` and `g()` are untyped - violations shouldn't be reported in any of those.

**Actual Behavior**

```
main.py:6: error: Name "UNDEFINED" is not defined
```

**Your Environment**

mypy-play, as linked above.


Discussion:
Good question! 

We don't check unannotated functions without `--check-untyped-defs` option.
But, `lambda` is not considered to be unannotated, because it cannot be annotated due to Python syntax limitations.
So, `lambda x: UNDEFINED in x` is considered to be typed. That's why it is checked.

I also agree with its conclusion, `UNDEFINED` is not defined.
I guess I can update docs to mention this special case.

Related https://github.com/python/mypy/issues/10895 and https://github.com/python/mypy/issues/3948
@hauntsaninja any ideas about the best place for such docs?
I think that adding a new section [here](https://mypy.readthedocs.io/en/latest/dynamic_typing.html) might be a good idea.
Another option for `lambda` is to inherit "annotated" status from its context:
- Not a function (module, class body) -> `lambda` is annotated
- Annotated function / method -> `lambda` is annotated
- Unannotated function / method -> `lambda` is not unnotated
+1 for inheriting annotated status from enclosing scope.

Patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -4961,9 +4961,26 @@ def in_checked_function(self) -> bool:
         - Yes in annotated functions.
         - No otherwise.
         """
-        return (self.options.check_untyped_defs
-                or not self.function_stack
-                or not self.function_stack[-1].is_dynamic())
+        if self.options.check_untyped_defs or not self.function_stack:
+            return True
+
+        current_index = len(self.function_stack) - 1
+        while current_index >= 0:
+            current_func = self.function_stack[current_index]
+            if (
+                isinstance(current_func, FuncItem)
+                and not isinstance(current_func, LambdaExpr)
+            ):
+                return not current_func.is_dynamic()
+
+            # Special case, `lambda` inherits the "checked" state from its parent.
+            # Because `lambda` itself cannot be annotated.
+            # `lambdas` can be deeply nested, so we try to find at least one other parent.
+            current_index -= 1
+
+        # This means that we only have a stack of `lambda` functions,
+        # no regular functions.
+        return True
 
     def fail(self,
              msg: str,


Test patch:
diff --git a/mypy/test/testsemanal.py b/mypy/test/testsemanal.py
--- a/mypy/test/testsemanal.py
+++ b/mypy/test/testsemanal.py
@@ -32,7 +32,8 @@
                  'semanal-typeddict.test',
                  'semenal-literal.test',
                  'semanal-classvar.test',
-                 'semanal-python2.test']
+                 'semanal-python2.test',
+                 'semanal-lambda.test']
 
 
 def get_semanal_options(program_text: str, testcase: DataDrivenTestCase) -> Options:
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
@@ -2366,6 +2366,19 @@ def f() -> None:
 [out]
 main:3: note: Revealed type is "builtins.int"
 
+[case testLambdaTypedContext]
+def f() -> None:
+    lambda: 'a'.missing()  # E: "str" has no attribute "missing"
+
+[case testLambdaUnypedContext]
+def f():
+    lambda: 'a'.missing()
+
+[case testLambdaCheckUnypedContext]
+# flags: --check-untyped-defs
+def f():
+    lambda: 'a'.missing()  # E: "str" has no attribute "missing"
+
 [case testEqNone]
 None == None
 [builtins fixtures/ops.pyi]
diff --git a/test-data/unit/semanal-lambda.test b/test-data/unit/semanal-lambda.test
new file mode 100644
--- /dev/null
+++ b/test-data/unit/semanal-lambda.test
@@ -0,0 +1,94 @@
+[case testLambdaInheritsCheckedContextFromFunc]
+def g():
+    return lambda x: UNDEFINED in x
+[out]
+MypyFile:1(
+  FuncDef:1(
+    g
+    Block:1(
+      ReturnStmt:2(
+        LambdaExpr:2(
+          Args(
+            Var(x))
+          Block:2(
+            ReturnStmt:2(
+              ComparisonExpr:2(
+                in
+                NameExpr(UNDEFINED)
+                NameExpr(x [l])))))))))
+
+[case testLambdaInheritsCheckedContextFromFuncForced]
+# flags: --check-untyped-defs
+def g():
+    return lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromTypedFunc]
+def g() -> None:
+    return lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromTypedFuncForced]
+# flags: --check-untyped-defs
+def g() -> None:
+    return lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromModule]
+g = lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromModuleForce]
+# flags: --check-untyped-defs
+g = lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromModuleLambdaStack]
+g = lambda: lambda: lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromModuleLambdaStackForce]
+# flags: --check-untyped-defs
+g = lambda: lambda: lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromFuncLambdaStack]
+def g():
+    return lambda: lambda: lambda x: UNDEFINED in x
+[out]
+MypyFile:1(
+  FuncDef:1(
+    g
+    Block:1(
+      ReturnStmt:2(
+        LambdaExpr:2(
+          Block:2(
+            ReturnStmt:2(
+              LambdaExpr:2(
+                Block:2(
+                  ReturnStmt:2(
+                    LambdaExpr:2(
+                      Args(
+                        Var(x))
+                      Block:2(
+                        ReturnStmt:2(
+                          ComparisonExpr:2(
+                            in
+                            NameExpr(UNDEFINED)
+                            NameExpr(x [l])))))))))))))))
+
+[case testLambdaInheritsCheckedContextFromFuncLambdaStackForce]
+# flags: --check-untyped-defs
+def g():
+    return lambda: lambda: lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromTypedFuncLambdaStack]
+def g() -> None:
+    return lambda: lambda: lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromTypedFuncLambdaStackForce]
+# flags: --check-untyped-defs
+def g() -> None:
+    return lambda: lambda: lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromClassLambdaStack]
+class A:
+    g = lambda: lambda: lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined
+
+[case testLambdaInheritsCheckedContextFromClassLambdaStackForce]
+# flags: --check-untyped-defs
+class A:
+    g = lambda: lambda: lambda x: UNDEFINED in x  # E: Name "UNDEFINED" is not defined