aosabook/500lines · error · UnboundLocalError

local variable '%s' referenced before assignment

Error message

local variable '%s' referenced before assignment

What it means

Raised by byte_LOAD_FAST when a name the compiler classified as a fast local is missing from frame.f_locals at the point of the load. Because a function assigns to the variable somewhere in its body, the compiler made it local and emitted LOAD_FAST/STORE_FAST; the interpreter consults ONLY f_locals (no globals/builtins fallback). This reproduces CPython's UnboundLocalError when a branch reads the local before the assignment runs.

Source

Thrown at interpreter/code/byterun/simple_python_interpreter.py:245

        elif name in frame.f_globals:
            val = frame.f_globals[name]
        elif name in frame.f_builtins:
            val = frame.f_builtins[name]
        else:
            raise NameError("name '%s' is not defined" % name)
        self.push(val)

    def byte_STORE_NAME(self, name):
        self.frame.f_locals[name] = self.pop()

    def byte_DELETE_NAME(self, name):
        del self.frame.f_locals[name]

    def byte_LOAD_FAST(self, name):
        if name in self.frame.f_locals:
            val = self.frame.f_locals[name]
        else:
            raise UnboundLocalError(
                "local variable '%s' referenced before assignment" % name
            )
        self.push(val)

    def byte_STORE_FAST(self, name):
        self.frame.f_locals[name] = self.pop()

    def byte_LOAD_GLOBAL(self, name):
        f = self.frame
        if name in f.f_globals:
            val = f.f_globals[name]
        elif name in f.f_builtins:
            val = f.f_builtins[name]
        else:
            raise NameError("global name '%s' is not defined" % name)
        self.push(val)

    ## Operators

View on GitHub (pinned to fba689d101)

Solutions

  1. Initialize the local before any read (assign it at the top of the function or on every branch).
  2. If the global was intended, declare `global name` so the compiler emits LOAD_GLOBAL instead of LOAD_FAST.
  3. Restructure the logic so the STORE_FAST always precedes the LOAD_FAST on every reachable path.

Example fix

// before
def f(flag):
    if flag: print(x)   # UnboundLocalError
    x = 1
// after
def f(flag):
    x = 0
    if flag: print(x)
Defensive patterns

Strategy: validation

Validate before calling

# static check: every LOAD_FAST name has a STORE_FAST that dominates it
import dis
fast_reads = {i.argval for i in dis.get_instructions(code) if i.opname == 'LOAD_FAST'}
fast_writes = {i.argval for i in dis.get_instructions(code) if i.opname == 'STORE_FAST'}
unsafe = fast_reads - fast_writes   # names read but never assigned
# at runtime, guard the read:
if name in frame.f_locals:
    val = frame.f_locals[name]
else:
    val = <default>

Try / catch

try:
    interpreter.run_frame(frame)
except UnboundLocalError as e:
    log.warning('local read before assign: %s', e)

Prevention

When it happens

Trigger: A function that assigns to `name` (forcing LOAD_FAST) but a code path reads `name` before any STORE_FAST runs, e.g. `def f(flag):\n if flag: print(x)\n x = 1`. Also from augmented assignment or conditional initialization that a skipped branch never executes.

Common situations: Adding a later `x = ...` inside a function that silently turns a previously-global `x` into a local; conditional initialization where one branch skips the assignment; refactor that moves an assignment inside an `if`.

Related errors


AI-assisted analysis of aosabook/500lines@fba689d101 (2026-08-13). Data as JSON: /api/errors/b425434cf34a413c. Report an issue: GitHub.