ruff/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md

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