Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 8 min read Last reviewed Jul 2026

Python Error Guide: 'UnboundLocalError: local variable referenced before assignment' — Fix Scope

Quick answer

Fix 'UnboundLocalError: local variable referenced before assignment' in Python: handle local scope, global shadowing, conditional and augmented-assignment bugs.

  • #python
  • #automation
  • #troubleshooting
  • #errors
Free toolkit

Stuck on this Bash & Python Automation error? Get the free incident triage checklist

A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.

Overview

UnboundLocalError is Python telling you that inside a function you read a variable that Python has decided is local — but you never assigned it a value before that read. Because Python determines a variable’s scope from whether it is assigned anywhere in the function (at compile time), a single assignment later in the body makes the name local for the whole function:

Traceback (most recent call last):
  File "job.py", line 20, in <module>
    run()
  File "job.py", line 15, in run
    print(count)
UnboundLocalError: local variable 'count' referenced before assignment

UnboundLocalError is a subclass of NameError, but its meaning is specific: the name exists as a local, it just has no value yet at the point you used it.

Symptoms

  • A function reads a variable that “obviously” has a value (a module-level global or a constant), yet raises UnboundLocalError.
  • Adding an assignment to a name anywhere in a function suddenly breaks an earlier read of that same name.
  • A variable assigned only inside an if/try branch is used later, and fails when that branch didn’t run.
  • +=, -=, or other augmented assignment on a global inside a function raises the error on the first use.
  • The error appears after refactoring — moving code into a function, or renaming/shadowing a global with a local of the same name.

Common Root Causes

  • Shadowing a global by assigning to it — reading a module-level variable and later assigning to the same name in the function makes the entire name local, so the earlier read fails.
  • Augmented assignment on a globaltotal += 1 where total is a global: += both reads and writes, so Python treats total as local and the read half fails because it was never assigned locally.
  • Conditional assignment — assigning a variable only inside an if/elif/try block, then using it on a path where the block didn’t execute.
  • Assignment inside a loop that may run zero times — using a loop variable or accumulator after a for/while that iterated zero times.
  • Early return/raise skipping the assignment — the assignment lives after a branch that exits early.
  • Typo creating a phantom local — assigning couunt while reading count, or vice versa.
  • try block failing before assignment — the assignment is in try, an exception skips it, and the except/finally reads the unassigned name.

Diagnostic Workflow

Read the traceback: the function and line tell you the read, and the variable name tells you what’s unbound. Then search that function for every assignment to the same name — an assignment anywhere makes it local:

grep -n 'count' job.py       # find all reads AND writes of the name in the function

Confirm Python’s scope decision with the compiler’s own view of the function’s locals:

import dis
dis.dis(run)     # LOAD_FAST on a name Python considers local vs LOAD_GLOBAL

If you see LOAD_FAST count (local) where you expected LOAD_GLOBAL count, Python has classified it as local because it is assigned somewhere in the function.

Reproduce the conditional-assignment case minimally:

def pick(x):
    if x > 0:
        label = "positive"
    return label     # UnboundLocalError when x <= 0

pick(-1)

Trace exactly which branch assigned (or didn’t) with a quick guard:

def pick(x):
    label = None            # ensure a definite binding on every path
    if x > 0:
        label = "positive"
    print("label bound to:", label)
    return label

Example Root Cause Analysis

A metrics job incremented a module-level counter from inside a function:

processed = 0

def handle(batch):
    for item in batch:
        do_work(item)
        processed += 1        # intended to bump the global
    return processed

It failed on the first call:

Traceback (most recent call last):
  File "metrics.py", line 8, in <module>
    handle(items)
  File "metrics.py", line 5, in handle
    processed += 1
UnboundLocalError: local variable 'processed' referenced before assignment

dis.dis(handle) showed LOAD_FAST processed — Python treated processed as local because processed += 1 assigns to it. += needs to read processed first, but there was no local processed yet, so the read failed. There are two correct fixes, and the choice matters for maintainability. If a true global counter is intended, declare it:

processed = 0

def handle(batch):
    global processed          # tell Python this name is the module-level one
    for item in batch:
        do_work(item)
        processed += 1
    return processed

The cleaner design avoids mutable global state entirely by using a local and returning the count:

def handle(batch):
    processed = 0             # local accumulator, initialized before use
    for item in batch:
        do_work(item)
        processed += 1
    return processed

total = handle(items)         # caller owns the running total

The second form is preferred in automation code — it’s testable, thread-safe, and free of hidden global mutation.

Prevention Best Practices

  • Initialize before conditional assignment — give every variable a definite value (even None) before any if/try that might assign it, so no code path can read it unbound.
  • Avoid mutating globals from functions — prefer passing values in and returning results; when you genuinely need module state, declare it explicitly with global (or nonlocal for closures) so the intent is visible.
  • Don’t rely on loop bodies running — after a for/while, don’t use a variable that’s only assigned inside the loop unless you initialized it beforehand.
  • Keep functions small — short functions make it obvious which names are assigned and where, so accidental shadowing is easy to spot.
  • Enable static checkspyflakes/ruff and pylint flag “local variable referenced before assignment” and “used before assignment” before you run the code.
  • Watch augmented assignment — remember x += 1, x |= ..., x[...] = ... on a bare name all count as assignment and make the name local.
  • Fail loudly in try — if an assignment lives in a try, handle the exception path explicitly rather than reading a maybe-unassigned name in except/finally.

Quick Command Reference

import dis
dis.dis(func)          # LOAD_FAST = local, LOAD_GLOBAL = global
global name            # inside a function: rebind the module-level name
nonlocal name          # inside a closure: rebind the enclosing function's name
x = None               # initialize before any conditional/loop assignment
grep -n 'NAME' file.py    # find every assignment to the name in the function
ruff check file.py        # static detection of use-before-assignment

Conclusion

UnboundLocalError is a scoping error, not a missing-value error: Python decided the name is local because it’s assigned somewhere in the function, and you read it before that assignment ran. The classic traps are shadowing a global you meant to read, augmented assignment (+=) on a global, and using a variable that was only assigned inside a conditional or loop. Fix it by initializing variables on every code path, declaring global/nonlocal when you truly intend to rebind outer state, and — best of all — preferring locals returned from small functions over mutating module globals.

Free download · 368-page PDF

Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.