ruff/crates/ty_python_semantic/resources/mdtest/scopes/global.md
Jack O'Connor ba070bb6d5
Some checks are pending
CI / cargo build (msrv) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
CI / mkdocs (push) Waiting to run
[ty] perform type narrowing for places marked global too (#19381)
Fixes https://github.com/astral-sh/ty/issues/311.
2025-07-22 16:42:10 -07:00

5.5 KiB

global references

Implicit global in function

A name reference to a never-defined symbol in a function is implicitly a global lookup.

x = 1

def f():
    reveal_type(x)  # revealed: Unknown | Literal[1]

Explicit global in function

x = 1

def f():
    global x
    reveal_type(x)  # revealed: Unknown | Literal[1]

Unassignable type in function

x: int = 1

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

    global x
    # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
    x = ""

    global z
    # error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
    z = ""

z: int

Nested intervening scope

A global statement causes lookup to skip any bindings in intervening scopes:

x: int = 1

def outer():
    x: str = ""

    def inner():
        global x
        reveal_type(x)  # revealed: int

Narrowing

An assignment following a global statement should narrow the type in the local scope after the assignment.

x: int | None

def f():
    global x
    x = 1
    reveal_type(x)  # revealed: Literal[1]

Same for an if statement:

x: int | None

def f():
    # The `global` keyword isn't necessary here, but this is testing that it doesn't get in the way
    # of narrowing.
    global x
    if x == 1:
        y: int = x  # allowed, because x cannot be None in this branch

nonlocal and global

A binding cannot be both nonlocal and global. This should emit a semantic syntax error. CPython marks the nonlocal line, while mypy, pyright, and ruff (PLE0115) mark the global line.

x = 1

def f():
    x = 1
    def g() -> None:
        nonlocal x
        global x  # error: [invalid-syntax] "name `x` is nonlocal and global"
        x = None

Global declaration after global statement

def f():
    global x
    y = x
    x = 1  # No error.

x = 2

Semantic syntax errors

Using a name prior to its global declaration in the same scope is a syntax error.

x = 1
y = 2

def f():
    print(x)
    global x  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    print(x)

def f():
    global x
    print(x)
    global x  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    print(x)

def f():
    print(x)
    global x, y  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    print(x)

def f():
    global x, y
    print(x)
    global x, y  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    print(x)

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

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

def f():
    del x
    global x, y  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    del x

def f():
    global x, y
    del x
    global x, y  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    del x

def f():
    del x
    global x  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    del x

def f():
    global x
    del x
    global x  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    del x

def f():
    del x
    global x, y  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    del x

def f():
    global x, y
    del x
    global x, y  # error: [invalid-syntax] "name `x` is used prior to global declaration"
    del x

def f():
    print(f"{x=}")
    global x  # error: [invalid-syntax] "name `x` is used prior to global declaration"

# still an error in module scope
x = None
global x  # error: [invalid-syntax] "name `x` is used prior to global declaration"

Local bindings override preceding global bindings

x = 42

def f():
    global x
    reveal_type(x)  # revealed: Unknown | Literal[42]
    x = "56"
    reveal_type(x)  # revealed: Literal["56"]

Local assignment prevents falling back to the outer scope

x = 42

def f():
    # error: [unresolved-reference] "Name `x` used when not defined"
    reveal_type(x)  # revealed: Unknown
    x = "56"
    reveal_type(x)  # revealed: Literal["56"]

Annotating a global binding is a syntax error

x: int = 1

def f():
    global x
    x: str = "foo"  # error: [invalid-syntax] "annotated name `x` can't be global"

Global declarations affect the inferred type of the binding

Even if the global declaration isn't used in an assignment, we conservatively assume it could be:

x = 1

def f():
    global x

# TODO: reveal_type(x)  # revealed: Unknown | Literal["1"]

Global variables need an explicit definition in the global scope

You're allowed to use the global keyword to define new global variables that don't have any explicit definition in the global scope, but we consider that fishy and prefer to lint on it:

x = 1
y: int
# z is neither bound nor declared in the global scope

def f():
    global x, y, z  # error: [unresolved-global] "Invalid global declaration of `z`: `z` has no declarations or bindings in the global scope"

You don't need a definition for implicit globals, but you do for built-ins:

def f():
    global __file__  # allowed, implicit global
    global int  # error: [unresolved-global] "Invalid global declaration of `int`: `int` has no declarations or bindings in the global scope"