7.5 KiB
Nonlocal references
One level up
def f():
x = 1
def g():
reveal_type(x) # revealed: Unknown | Literal[1]
Two levels up
def f():
x = 1
def g():
def h():
reveal_type(x) # revealed: Unknown | Literal[1]
Skips class scope
def f():
x = 1
class C:
x = 2
def g():
reveal_type(x) # revealed: Unknown | Literal[1]
Skips annotation-only assignment
def f():
x = 1
def g():
# it's pretty weird to have an annotated assignment in a function where the
# name is otherwise not defined; maybe should be an error?
x: int
def h():
reveal_type(x) # revealed: Unknown | Literal[1]
The nonlocal keyword
Without the nonlocal keyword, x += 1 is not allowed in an inner scope, even if we break it up
into multiple steps. Local variable scoping is "forward-looking" in the sense that even a later
assignment of x means that all reads of x in that scope only look at that scope's binding:
def f():
x = 1
def g():
x += 1 # error: [unresolved-reference]
def f():
x = 1
def g():
y = x # error: [unresolved-reference]
x = y + 1
With nonlocal these examples work, because the reassignments modify the variable from the outer
scope rather than modifying a separate variable local to the inner scope:
def f():
x = 1
def g():
nonlocal x
x += 1
reveal_type(x) # revealed: Unknown | Literal[2]
def f():
x = 1
def g():
nonlocal x
y = x
x = y + 1
reveal_type(x) # revealed: Unknown | Literal[2]
def f():
x = 1
y = 2
def g():
nonlocal x, y
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(y) # revealed: Unknown | Literal[2]
nonlocal declarations must match an outer binding
nonlocal x isn't allowed when there's no binding for x in an enclosing scope:
def f():
def g():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
def f():
x = 1
def g():
nonlocal x, y # error: [invalid-nonlocal] "no binding for nonlocal `y` found"
A global x doesn't work either; the outer-scope binding for the variable has to originate from a
function-like scope:
x = 1
def f():
def g():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
A class-scoped x also doesn't work, for the same reason:
class Foo:
x = 1
@staticmethod
def f():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
nonlocal uses the closest binding
def f():
x = 1
def g():
x = 2
def h():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[2]
nonlocal "chaining"
Multiple nonlocal statements can "chain" through nested scopes:
def f():
x = 1
def g():
nonlocal x
def h():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
And the nonlocal chain can skip over a scope that doesn't bind the variable:
def f1():
x = 1
def f2():
nonlocal x
def f3():
# No binding; this scope gets skipped.
def f4():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
But a global statement breaks the chain:
def f():
x = 1
def g():
global x
def h():
nonlocal x # error: [invalid-nonlocal] "no binding for nonlocal `x` found"
nonlocal bindings respect declared types from the defining scope
By default (without nonlocal), an inner variable shadows an outer variable of the same name, and
type declarations from the outer scope don't apply to the inner one:
def f():
x: int = 1
def g():
# `Literal["string"]` is not assignable to `int` # (the declared type in the outer scope),
# but we don't emit a diagnostic complaining about it because `x` in the inner scope is a
# distinct variable; the outer-scope declarations do not apply to it.
x = "string"
But when x is nonlocal, type declarations from the defining scope apply to it:
def f():
x: int = 1
def g():
nonlocal x
x = "string" # error: [invalid-assignment] "Object of type `Literal["string"]` is not assignable to `int`"
This is true even if the outer scope declares x without binding it:
def f():
x: int
def g():
nonlocal x
x = "string" # error: [invalid-assignment] "Object of type `Literal["string"]` is not assignable to `int`"
We can "see through" multiple layers of nonlocal statements, and also through scopes that don't
bind the variable at all. However, we don't see through global statements:
def f1():
# The original bindings of `x`, `y`, and `z` with type declarations.
x: int = 1
y: int = 1
z: int = 1
def f2():
# This scope doesn't touch `x`, `y`, or `z` at all.
def f3():
# This scope treats declares `x` nonlocal and `y` as global, and it shadows `z` without
# giving it a new type declaration.
nonlocal x
x = 2
global y
y = 2
z = 2
def f4():
# This scope sees `x` from `f1` and `z` from `f3`, but it doesn't see `y` at all,
# because of the `global` keyword above.
nonlocal x, y, z # error: [invalid-nonlocal] "no binding for nonlocal `y` found"
x = "string" # error: [invalid-assignment]
z = "string" # not an error
TODO: nonlocal affects the inferred type in the outer scope
Without nonlocal, g can't write to x, and the inferred type of x in f's scope isn't
affected by g:
def f():
x = 1
def g():
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(x) # revealed: Literal[1]
But with nonlocal, g could write to x, and that affects its inferred type in f. That's true
regardless of whether g actually writes to x. With a write:
def f():
x = 1
def g():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
x += 1
reveal_type(x) # revealed: Unknown | Literal[2]
# TODO: should be `Unknown | Literal[1]`
reveal_type(x) # revealed: Literal[1]
Without a write:
def f():
x = 1
def g():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
# TODO: should be `Unknown | Literal[1]`
reveal_type(x) # revealed: Literal[1]
Annotating a nonlocal binding is a syntax error
def f():
x: int = 1
def g():
nonlocal x
x: str = "foo" # error: [invalid-syntax] "annotated name `x` can't be nonlocal"
nonlocal after use
Using a name prior to its nonlocal declaration in the same scope is a syntax error:
def f():
x = 1
def g():
print(x)
nonlocal x # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"
nonlocal before outer initialization
nonlocal x works even if x isn't bound in the enclosing scope until afterwards (since the
function defining the inner scope might only be called after the later binding!):
def f():
def g():
def h():
nonlocal x
reveal_type(x) # revealed: Unknown | Literal[1]
x = 1