ruff/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md
2025-07-11 09:44:54 -07:00

8.2 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, bindings in an inner scope shadow variables of the same name in enclosing scopes. This example isn't a type error, because the inner x shadows the outer one:

def f():
    x: int = 1
    def g():
        x = "hello"  # allowed

With nonlocal it is a type error, because x refers to the same place in both scopes:

def f():
    x: int = 1
    def g():
        nonlocal x
        x = "hello"  # error: [invalid-assignment] "Object of type `Literal["hello"]` is not assignable to `int`"

Local variable bindings "look ahead" to any assignment in the current scope

The binding x = 2 in g causes the earlier read of x to refer to g's not-yet-initialized binding, rather than to x = 1 in f's scope:

def f():
    x = 1
    def g():
        if x == 1:  # error: [unresolved-reference] "Name `x` used when not defined"
            x = 2

The nonlocal keyword makes this example legal (and makes the assignment x = 2 affect the outer scope):

def f():
    x = 1
    def g():
        nonlocal x
        if x == 1:
            x = 2

For the same reason, using the += operator in an inner scope is an error without nonlocal (unless you shadow the outer variable first):

def f():
    x = 1
    def g():
        x += 1  # error: [unresolved-reference] "Name `x` used when not defined"

def f():
    x = 1
    def g():
        x = 1
        x += 1  # allowed, but doesn't affect the outer scope

def f():
    x = 1
    def g():
        nonlocal x
        x += 1  # allowed, and affects the outer scope

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-syntax] "no binding for nonlocal `x` found"

def f():
    x = 1
    def g():
        nonlocal x, y  # error: [invalid-syntax] "no binding for nonlocal `y` found"

A global x doesn't work. The target must be in a function-like scope:

x = 1

def f():
    def g():
        nonlocal x  # error: [invalid-syntax] "no binding for nonlocal `x` found"

def f():
    global x
    def g():
        nonlocal x  # error: [invalid-syntax] "no binding for nonlocal `x` found"

A class-scoped x also doesn't work:

class Foo:
    x = 1
    @staticmethod
    def f():
        nonlocal x  # error: [invalid-syntax] "no binding for nonlocal `x` found"

However, class-scoped bindings don't break the nonlocal chain the way global declarations do:

def f():
    x: int = 1

    class Foo:
        x: str = "hello"

        @staticmethod
        def g():
            # Skips the class scope and reaches the outer function scope.
            nonlocal x
            x = 2  # allowed
            x = "goodbye"  # error: [invalid-assignment]

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-syntax] "no binding for nonlocal `x` found"

nonlocal bindings respect declared types from the defining scope, even without a binding

def f():
    x: int
    def g():
        nonlocal x
        x = "string"  # error: [invalid-assignment] "Object of type `Literal["string"]` is not assignable to `int`"

A complicated mixture of nonlocal chaining, empty scopes, class scopes, and the global keyword

def f1():
    # The original bindings of `x`, `y`, and `z` with type declarations.
    x: int = 1
    y: int = 2
    z: int = 3

    def f2():
        # This scope doesn't touch `x`, `y`, or `z` at all.

        class Foo:
            # This class scope is totally ignored.
            x: str = "a"
            y: str = "b"
            z: str = "c"

            @staticmethod
            def f3():
                # This scope declares `x` nonlocal and `y` as global, and it shadows `z` without
                # giving it a type declaration.
                nonlocal x
                x = 4
                y = 5
                global z
                z = 6

                def f4():
                    # This scope sees `x` from `f1` and `y` from `f3`. It *can't* declare `z`
                    # nonlocal, because of the global statement above, but it *can* load `z` as a
                    # "free" variable, in which case it sees the global value.
                    nonlocal x, y, z  # error: [invalid-syntax] "no binding for nonlocal `z` found"
                    x = "string"  # error: [invalid-assignment]
                    y = "string"  # allowed, because `f3`'s `y` is untyped
                    reveal_type(z)  # revealed: Unknown | Literal[6]

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"

Use before nonlocal

Using a name prior to its nonlocal declaration in the same scope is a syntax error:

def f():
    x = 1
    def g():
        x = 2
        nonlocal x  # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"

This is true even if there are multiple nonlocal declarations of the same variable, as long as any of them come after the usage:

def f():
    x = 1
    def g():
        nonlocal x
        x = 2
        nonlocal x  # error: [invalid-syntax] "name `x` is used prior to nonlocal declaration"

def f():
    x = 1
    def g():
        nonlocal x
        nonlocal x
        x = 2  # allowed

nonlocal before outer initialization

nonlocal x works even if x isn't bound in the enclosing scope until afterwards:

def f():
    def g():
        # This is allowed, because of the subsequent definition of `x`.
        nonlocal x
    x = 1