The Global and Nonlocal Statement Traps

Python decides whether a name inside a function is local statically, at compile time, by scanning the whole function body once: if the name is assigned anywhere in that body — even on a line that never runs, even after the read that fails — the compiler marks it local for the entire function, and any read that executes before the assignment raises UnboundLocalError (Programming FAQ). The fixes are the global and nonlocal statements, which override that classification — but each carries its own traps: global is needed only to rebind a module name, never to mutate one; nonlocal requires a name already bound in an enclosing function or it is a compile-time SyntaxError; and neither may name a parameter (language reference §7.12–7.13). This note is about those traps — the symptoms, why they fire, and how to fix them. The static scope-classification rules themselves (LEGB resolution, what counts as a binding) are derived in Local Global and Nonlocal Scopes and the symbol table that records them in Symbol Table Construction; here we cross-link that machinery and focus on where it bites.

Mental Model

The mental model that makes every one of these errors obvious: a function’s set of local names is frozen at compile time, before a single line runs. When CPython compiles a function it builds a symbol table (see Symbol Table Construction) by scanning the body for bindings — assignments, def, for targets, import, with ... as, function parameters, and so on. Any name that is bound somewhere in the body is a local for the whole body; any name only ever read is treated as global/enclosing. This classification is positional-blind: it does not matter that the read comes before the assignment textually, and it does not matter whether the assignment line ever executes. The decision is made once, for the entire function, from the mere presence of a binding.

flowchart TD
    A["compile function body"] --> B{"is name X bound<br/>ANYWHERE in body?"}
    B -->|no| C["X is global/enclosing<br/>(reads look outward)"]
    B -->|yes| D["X is LOCAL for the<br/>WHOLE body"]
    D --> E{"override declared?"}
    E -->|global X| F["X is module-global<br/>reads & writes hit module"]
    E -->|nonlocal X| G["X binds nearest enclosing<br/>function local<br/>(must already exist)"]
    E -->|none| H["read before the binding<br/>runs ->  UnboundLocalError"]
    style H fill:#ffd6d6
    style F fill:#d6ffd6
    style G fill:#d6ffd6

Diagram: the compile-time decision tree for one name. The single insight: the red UnboundLocalError branch is reached purely because a binding exists somewhere in the body — moving the read earlier cannot help, because locality is decided for the whole function at once. global/nonlocal are the two escape hatches that redirect the green branches.

The Canonical Bug — UnboundLocalError

The textbook reproduction, run live on CPython 3.14.5:

g = 10
def f():
    print(g)        # intends to read the global g
    g = 20          # but this assignment makes g LOCAL for all of f
f()
# UnboundLocalError: cannot access local variable 'g' where it is not associated with a value

The author’s intent is “read the global g, then create a local g.” Python’s actual reading is “g is assigned in f, therefore g is local throughout f; the print(g) reads a local that has not been assigned yet.” The error fires on the print line — before the assignment that “caused” it ever runs. The FAQ states the rule plainly: “If a variable is assigned a value anywhere within the function’s body, it’s assumed to be a local unless explicitly declared as global,” and “the compiler recognizes it as a local variable” from the assignment regardless of position (Programming FAQ).

Error-message wording changed in 3.11

The live 3.14.5 message is cannot access local variable 'g' where it is not associated with a value. The older wording — local variable 'g' referenced before assignment — is what most pre-3.11 documentation and the bulk of Stack Overflow answers still show (CPython improved these messages in 3.11). They are the same UnboundLocalError; do not be thrown if a search result shows the older phrasing. The note title uses the historically famous wording for discoverability.

UnboundLocalError is a subclass of NameError (verified live: issubclass(UnboundLocalError, NameError) → True), so a bare except NameError will catch it (exceptions reference).

Why — the bytecode tells the story

The classification is visible in the compiled code. Disassembling f on 3.14.5 (full output):

  4   RESUME             0
  5   LOAD_GLOBAL        1 (print + NULL)
      LOAD_FAST_CHECK    0 (g)        <-- g is a FAST (local) slot, with an init check
      CALL               1
      POP_TOP
  6   LOAD_SMALL_INT     20
      STORE_FAST         0 (g)        <-- and it is STORED as a local
      LOAD_CONST         1 (None)
      RETURN_VALUE
# f.__code__.co_varnames -> ('g',)    <-- g is in the LOCAL names
# f.__code__.co_names    -> ('print',)

The reads-and-writes of g compile to LOAD_FAST_CHECK / STORE_FAST — the fast-locals array machinery, not LOAD_GLOBAL. And g appears in co_varnames (the local-variable names), confirming the compiler classified it local before execution. The relevant opcode is LOAD_FAST_CHECK, which “pushes a reference to the local… raising an UnboundLocalError if the local variable has not been initialized” (dis reference); it was added in CPython 3.12 specifically so that the unchecked LOAD_FAST could be used wherever the compiler can prove the local is already initialized. So the exact opcode you see depends on whether the compiler can prove initialization at that point — here it cannot, so it emits the checked variant, which is what raises at runtime. The fast-locals array itself is covered in Fast Locals and the LOAD_FAST Family.

Augmented assignment is the sneakiest trigger

x += 1 reads x, adds, and stores back to x — so it is an assignment to x, and it makes x local for the whole function. A function that only ever does x += 1 against a global x, with no other reference, still fails. Verified on 3.14.5:

count = 0
def inc():
    count += 1      # count = count + 1 : the read needs a value, but count is local & unassigned
inc()
# UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

This one surprises people who reason “I’m not assigning, I’m just incrementing the existing global.” Augmented assignment is assignment for scope purposes. The same logic explains del x followed by a read: del is a binding operation that makes x local, and after the del the slot is unbound, so a subsequent read raises the identical error (verified: del x; print(x)UnboundLocalError).

The except ... as e deletion trap

A particularly subtle member of the family: the exception name bound by except SomeError as e is implicitly deleted at the end of the except block, so referencing it afterward raises UnboundLocalError — not NameError. The language reference is explicit that the binding is cleared, translating except E as N: foo into a try: foo finally: del N, and notes the reason: “Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs” (try statement reference). Verified on 3.14.5:

def h():
    try:
        raise ValueError("x")
    except ValueError as e:
        pass
    return e          # e was del'd at the end of the except block
h()
# UnboundLocalError: cannot access local variable 'e' where it is not associated with a value

The error is UnboundLocalError, not NameError, precisely because the name was bound in the function (so the compiler classifies e as a local) and then deleted — the same “local, but currently unbound” state as the del x case. The fix is to copy the exception out before the block ends: except ValueError as e: err = e and then use err, which is an ordinary local and survives. This trips people who want to log or re-raise an exception captured several lines later; the captured name has quietly evaporated.

The Fixes — and Their Own Traps

global — for rebinding a module name

To assign to a module-level name from inside a function, declare it global. Verified on 3.14.5:

g = 10
def fix():
    global g
    print(g)        # 10  -- now reads the module global
    g = 20          # rebinds the module global
fix()
# g is now 20

Disassembly confirms the redirection: with global g, the reference compiles to LOAD_GLOBAL/STORE_GLOBAL and g vanishes from co_varnames (verified: fix.__code__.co_varnames → ()). The language reference: “It would be impossible to assign to a global variable without global, although free variables may refer to globals without being declared global” (§7.12).

The trap inside the fix: global is needed only to rebind the name, never to mutate the object it points at. If the global is a mutable container and you only mutate it in place, no global is required, because there is no assignment to the name:

registry = {}
def add(k, v):
    registry[k] = v     # mutates the dict the global name points at — NO global needed, NO error

Reaching for global registry here is a common cargo-cult overcorrection; it is unnecessary and signals a misunderstanding of bind-vs-mutate. Two further global traps, both compile-time SyntaxErrors verified on 3.14.5:

  • A name cannot be both a parameter and global: def p(a):\n global aSyntaxError: name 'a' is parameter and global. A parameter is already a local binding; declaring it global is contradictory.
  • global must precede any use in the scope: “A SyntaxError is raised if a variable is used or assigned to prior to its global declaration in the scope” (§7.12). Put the declaration at the top of the function.

Also note global “applies only to code parsed at the same time” — a global inside a string passed to exec() does not affect the surrounding function (§7.12).

nonlocal — for rebinding an enclosing function’s local

Inside a nested function, nonlocal rebinds a name in the nearest enclosing function scope (not the module). This is how closures mutate captured state. Verified on 3.14.5:

def outer():
    n = 0
    def inner():
        nonlocal n
        n += 1          # rebinds outer's n via the enclosing cell
    inner(); inner()
    return n            # 2

The mechanism — nonlocal names become cell variables shared between the frames — is detailed in Cell Variables and Closures; here we care about the traps.

The defining nonlocal trap: the name must already be bound in an enclosing function scope, or it is a compile-time SyntaxError. There is nothing for nonlocal to attach to otherwise. Verified on 3.14.5:

def o():
    def i():
        nonlocal z      # z is not bound in o (or any enclosing function)
        z = 1
    i()
# SyntaxError: no binding for nonlocal 'z' found

The reference: “If a name is not bound in any nonlocal scope, or if there is no nonlocal scope, a SyntaxError is raised” (§7.13). Two corollaries verified on 3.14.5:

  • nonlocal is illegal at module level: nonlocal q at top level → SyntaxError: nonlocal declaration not allowed at module level. The module is not a function scope, so there is no enclosing function local to bind. (To touch a module global from a nested function, use global, not nonlocal.)
  • Like global, nonlocal cannot name a parameter of its own function and must precede first use in the scope (§7.13).

And the same bind-vs-mutate distinction applies: to mutate an enclosing function’s mutable object you need no nonlocal at all — only to rebind the name. Verified on 3.14.5:

def outer():
    cfg = {'n': 0}
    def inner():
        cfg['n'] += 1   # mutates the dict — no nonlocal needed
    inner()
    return cfg          # {'n': 1}

Whereas rebinding does need it:

def outer2():
    n = 0
    def inner():
        n = n + 1       # rebind attempt -> n is local to inner, unbound on the read
    inner()
# UnboundLocalError: cannot access local variable 'n' where it is not associated with a value

This last case is the enclosing-scope twin of the canonical global bug: inner assigns n, so n is local to inner, so the read fails — and the fix is nonlocal n. The same compile-time classification, one scope level in.

Connection to Closures-Over-Loop-Variables

Scope traps cluster. The closely related nonlocal machinery — names captured by cells from an enclosing scope — is what produces the late-binding closure surprise: a lambda created inside a for loop captures the loop variable, not its value-at-creation, so all the closures see the loop variable’s final value. Verified on 3.14.5: [lambda: i for i in range(3)] then calling each yields [2, 2, 2], not [0, 1, 2]. That is a different defect (late binding of a free variable) but it shares the same underlying model — names, not values, are captured, and scope is resolved by the compiler — and it is treated in full in Late Binding Closures. The unifying lesson across all of these: Python binds names statically and resolves them late; reason about which scope a name belongs to before reasoning about when code runs.

Failure Modes and Diagnosis

The symptoms and their five-second diagnoses:

  • UnboundLocalError on a name you “know” is global. Search the function for any assignment to that name — including +=, -=, for name in ..., with ... as name, except ... as name, import name, and del name. Find it, and you have found why the name is local. Decide: did you mean to read the global (add global/nonlocal, or rename the local) or to use a fresh local (initialize it before the read)?
  • UnboundLocalError on an exception variable after its except block. The except E as e name is auto-deleted at the block’s end; copy it (err = e) inside the block to use it later.
  • SyntaxError: no binding for nonlocal 'x' found. You declared nonlocal x but no enclosing function binds x. Either the binding lives in the module (use global) or it does not exist yet (create it in the enclosing function first).
  • SyntaxError: name 'a' is parameter and global (or ... and nonlocal). You declared a parameter as global/nonlocal. Remove the declaration; rename one of them.
  • “My global/nonlocal had no effect.” You wrote it inside a string fed to exec/eval, which does not affect the enclosing real function (§7.12).
  • “I added global but the value still doesn’t change for callers.” You were mutating, not rebinding — the global was never needed, and your real bug is elsewhere (often you rebound a local copy you thought was the global, or you mutated a copy).

The most reliable probe is func.__code__.co_varnames: if the name is in there, the compiler made it local, full stop. Pair it with dis.dis(func) to see whether the access is LOAD_FAST*/STORE_FAST (local) or LOAD_GLOBAL/STORE_GLOBAL (global) — the bytecode never lies about classification.

Production Notes

These are among the first errors every Python programmer meets, and the global-only-for-rebinding subtlety trips even experienced engineers, which is why the official Programming FAQ devotes a dedicated entry to it (Programming FAQ). In real code the bug most often appears when someone adds an assignment to a previously read-only global deep inside a long function — the new line silently reclassifies the name for the whole function and breaks an earlier read that had worked for years. The defensive habits: keep global/nonlocal declarations at the very top of the function so the classification is obvious; prefer passing state as arguments and returning results over mutating module globals (which the global keyword’s friction is quietly discouraging); and when you do need shared mutable state, mutate the container in place rather than rebinding, sidestepping the keywords entirely. Linters such as pyflakes and pylint detect many UnboundLocalError shapes statically, before the code runs.

See Also