diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md index 7edb116cc5..acfb618d6a 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md @@ -44,6 +44,24 @@ def f(): reveal_type(x) # revealed: str ``` +## Reads terminate at the `global` keyword in an enclosing scope, even if there's no binding in that scope + +_Unlike_ variables that are explicitly declared `nonlocal` (below), implicitly nonlocal ("free") +reads can come from a variable that's declared `global` in an enclosing scope. It doesn't matter +whether the variable is bound in that scope: + +```py +x: int = 1 + +def f(): + x: str = "hello" + def g(): + global x + def h(): + # allowed: this loads the global `x` variable due to the `global` declaration in the immediate enclosing scope + y: int = x +``` + ## The `nonlocal` keyword Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in @@ -264,8 +282,8 @@ def f1(): @staticmethod def f3(): - # This scope declares `x` nonlocal and `y` as global, and it shadows `z` without - # giving it a type declaration. + # This scope declares `x` nonlocal, shadows `y` without a type declaration, and + # declares `z` global. nonlocal x x = 4 y = 5 diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 1b94e27073..2feced1ea3 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -6079,6 +6079,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(enclosing_place) = enclosing_place_table.place_by_expr(expr) else { continue; }; + if enclosing_place.is_marked_global() { + // Reads of "free" variables can terminate at an enclosing scope that marks the + // variable `global` but doesn't actually bind it. In that case, stop walking + // scopes and proceed to the global handling below. (But note that it's a + // semantic syntax error for the `nonlocal` keyword to do this. See + // `infer_nonlocal_statement`.) + break; + } if enclosing_place.is_bound() || enclosing_place.is_declared() { // We can return early here, because the nearest function-like scope that // defines a name must be the only source for the nonlocal reference (at