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 theglobalandnonlocalstatements, which override that classification — but each carries its own traps:globalis needed only to rebind a module name, never to mutate one;nonlocalrequires a name already bound in an enclosing function or it is a compile-timeSyntaxError; 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 valueThe 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 sameUnboundLocalError; 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 valueThis 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 valueThe 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 20Disassembly 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 errorReaching 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 a→SyntaxError: name 'a' is parameter and global. A parameter is already a local binding; declaring it global is contradictory. globalmust precede any use in the scope: “ASyntaxErroris 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 # 2The 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' foundThe 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:
nonlocalis illegal at module level:nonlocal qat 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, useglobal, notnonlocal.)- Like
global,nonlocalcannot 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 valueThis 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:
UnboundLocalErroron 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, anddel name. Find it, and you have found why the name is local. Decide: did you mean to read the global (addglobal/nonlocal, or rename the local) or to use a fresh local (initialize it before the read)?UnboundLocalErroron an exception variable after itsexceptblock. Theexcept E as ename 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 declarednonlocal xbut no enclosing function bindsx. Either the binding lives in the module (useglobal) 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 asglobal/nonlocal. Remove the declaration; rename one of them.- “My
global/nonlocalhad no effect.” You wrote it inside a string fed toexec/eval, which does not affect the enclosing real function (§7.12). - “I added
globalbut the value still doesn’t change for callers.” You were mutating, not rebinding — theglobalwas 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
- Local Global and Nonlocal Scopes — the LEGB resolution and static scope-classification rules this gotcha exploits; the mechanism, not the trap.
- Symbol Table Construction — where the compiler records each name’s local/global/cell classification.
- Cell Variables and Closures — how
nonlocalnames become shared cells between frames. - Fast Locals and the LOAD_FAST Family — the fast-locals array and the
LOAD_FAST/LOAD_FAST_CHECK/STORE_FASTopcodes. - Late Binding Closures — the loop-variable capture trap that shares this static-binding model.
- Python Internals MOC — parent map of content.