Python Error Guide: 'UnboundLocalError: local variable referenced before assignment' — Fix Scope
Fix 'UnboundLocalError: local variable referenced before assignment' in Python: handle local scope, global shadowing, conditional and augmented-assignment bugs.
- #python
- #automation
- #troubleshooting
- #errors
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/trybranch 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 global —
total += 1wheretotalis a global:+=both reads and writes, so Python treatstotalas local and the read half fails because it was never assigned locally. - Conditional assignment — assigning a variable only inside an
if/elif/tryblock, 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/whilethat iterated zero times. - Early
return/raiseskipping the assignment — the assignment lives after a branch that exits early. - Typo creating a phantom local — assigning
couuntwhile readingcount, or vice versa. tryblock failing before assignment — the assignment is intry, an exception skips it, and theexcept/finallyreads 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 anyif/trythat 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(ornonlocalfor 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 checks —
pyflakes/ruffandpylintflag “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 atry, handle the exception path explicitly rather than reading a maybe-unassigned name inexcept/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.
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?
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.