Python raises UnboundLocalError: cannot access local variable 'x' where it is not associated with a value when a function reads a name that Python has decided is local to that function, but the line that gives it a value has not run yet. Any assignment to a name anywhere inside a function (including +=, for, import, del and except ... as) makes that name local for the whole function. The usual fixes are to give the variable a value before any branch or loop, pass values in and return them, or declare global or nonlocal when you really mean an outer variable.
On Python 3.10 and older the same error reads UnboundLocalError: local variable 'x' referenced before assignment. It is the same problem with a friendlier message in newer versions. The Python docs list UnboundLocalError as a subclass of NameError, raised for a local variable that has no value bound to it yet.
The confusing part is that the variable often does exist, right at the top of the file. I hit it myself on my first visit counter. I reproduced every example below on Python 3.13.5, and I re-ran the first one on 3.10.22, 3.11.17, 3.14.8 and 3.15.0rc3 to compare the messages. Paths in the tracebacks point at my test scripts under /workspace/seo-posts-nov08/test/python/.
In this guide:
- What the error looks like
- Why Python thinks the variable is local
- The 10 causes at a glance
- 1. Augmented assignment (+=) to a global
- 2. Reading a global, then assigning it later
- 3. A variable assigned only inside an if branch
- 4. Using a loop variable after an empty loop
- 5. A try block that failed before the assignment
- 6. A nested function updating an outer variable
- 7. Reading a name after del
- 8. An import later in the same function
- 9. Using an except ... as variable after the block
- 10. Shadowing a built-in like len
- How to find the cause in under a minute
- Frequently asked questions
What the error looks like
Here is the classic version: a module-level counter and a function that tries to bump it.
count = 0 def add_visit(): count += 1 return count print(add_visit()) Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c01_global_counter.py", line 7, in <module> print(add_visit()) ~~~~~~~~~^^ File "/workspace/seo-posts-nov08/test/python/c01_global_counter.py", line 4, in add_visit count += 1 ^^^^^ UnboundLocalError: cannot access local variable 'count' where it is not associated with a value The carets point at count on the += line. Python 3.11.17, 3.14.8 and 3.15.0rc3 printed the same final message. Python 3.10.22 printed the older wording, UnboundLocalError: local variable 'count' referenced before assignment, without the caret lines. Same bug, same fixes.
Why Python thinks the variable is local
Python decides the scope of every name when it compiles a function, before any line runs. The rule from the execution model is simple: if a name is bound anywhere in a function body, it is local to that whole function, unless you declare it global or nonlocal. Position does not matter. An assignment on the last line still makes the name local on the first line.
So count += 1 means "read local count, add 1, store local count". The read happens first, the local has no value yet, and Python refuses to quietly fall back to the global with the same name. That refusal is a good thing. A silent fallback would make bugs far harder to spot.
You can watch the compiler's decision yourself. This script inspects the function before calling it:
total = 100 def report(): print("total is", total) total = 0 print("UnboundLocalError is NameError subclass:", issubclass(UnboundLocalError, NameError)) print("local names in report:", report.__code__.co_varnames) import symtable src = open(__file__).read() table = symtable.symtable(src, __file__, "exec") fn = table.lookup("report").get_namespaces()[0] sym = fn.lookup("total") print("total is_local:", sym.is_local(), "is_global:", sym.is_global()) try: report() except NameError as e: print("caught by except NameError:", type(e).__name__, "-", e) UnboundLocalError is NameError subclass: True local names in report: ('total',) total is_local: True is_global: False caught by except NameError: UnboundLocalError - cannot access local variable 'total' where it is not associated with a value co_varnames and symtable both list total as local before the function runs. And because UnboundLocalError subclasses NameError, except NameError catches both.
The 10 causes at a glance
Every cause below comes back to the same rule: something assigns the name inside the function, and a read runs before that assignment. Use this table to jump to the one that matches your code.
| # | Cause | What you see | Fix |
|---|---|---|---|
| 1 | count += 1 on a global | Fails on the first call | Return a new value, or declare global |
| 2 | Read a global, assign it later in the same function | Fails on the read, not the assignment | Rename the local or use global |
| 3 | Assigned only inside an if | Works for some inputs only | Set a default before the if |
| 4 | Loop variable after an empty loop | Works until the list is empty | Initialise before the loop |
| 5 | try failed before the assignment | Error inside except/finally | Use with or set None first |
| 6 | Closure updates an outer variable | Inner function fails on += | Declare nonlocal |
| 7 | del x, then read x | Fails after the delete | Do not reuse a deleted name |
| 8 | import later in the same function | Module name fails at the top of the function | Move the import to the top of the file |
| 9 | except ... as err used after the block | Fails after the handler ends | Copy err to another variable |
| 10 | Built-in name like len reassigned later | Built-in suddenly "missing" | Pick a different variable name |
1. Augmented assignment (+=) to a global
This is the example from the top of the post, and it is the most common cause by far. Counters, running totals and "last seen" values all hit it. +=, -=, *= and friends all count as assignments, so the name becomes local.
The cleanest fix is to stop mutating the global at all: pass the value in and return the new one. If the function really must change module-level state, declare it with the global statement. Here are both, plus the fixes for causes 3, 4 and 6, from one test run:
# Fix 1: pass in and return instead of mutating a global def add_visit(count): return count + 1 visits = 0 visits = add_visit(visits) print("returned:", visits) # Fix 2: global, when you really mean the module-level name count = 0 def add_visit_global(): global count count += 1 return count print("global:", add_visit_global(), add_visit_global()) # Fix 3: nonlocal for closures def make_counter(): n = 0 def increment(): nonlocal n n += 1 return n return increment c = make_counter() print("nonlocal:", c(), c(), c()) # Fix 4: initialise before branches and loops def label(score): result = "fail" if score >= 50: result = "pass" return result print("label:", label(72), label(30)) def last_item(items, default=None): item = default for item in items: pass return item print("last_item:", last_item(["a", "b"]), last_item([])) returned: 1 global: 1 2 nonlocal: 1 2 3 label: pass fail last_item: b None I prefer the return-value version: it keeps functions easy to test.
2. Reading a global, then assigning it later
This one surprises people because the failing line is a plain read. The assignment that causes it sits further down:
total = 100 def report(): print("total is", total) total = 0 report() Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c02_read_then_assign.py", line 7, in <module> report() ~~~~~~^^ File "/workspace/seo-posts-nov08/test/python/c02_read_then_assign.py", line 4, in report print("total is", total) ^^^^^ UnboundLocalError: cannot access local variable 'total' where it is not associated with a value The traceback points at the print, not at total = 0. Whenever the caret lands on a harmless-looking read, scan the rest of the function for any assignment to the same name. Usually you meant a different variable, so rename the local (for example new_total = 0). If you really meant to reset the global, add global total as the first line of the function.
3. A variable assigned only inside an if branch
Here the function works for some inputs and crashes for others, which makes it easy to ship by accident:
def label(score): if score >= 50: result = "pass" return result print(label(72)) print(label(30)) pass Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c03_if_branch.py", line 7, in <module> print(label(30)) ~~~~~^^^^ File "/workspace/seo-posts-nov08/test/python/c03_if_branch.py", line 4, in label return result ^^^^^^ UnboundLocalError: cannot access local variable 'result' where it is not associated with a value A score of 72 takes the if branch and returns "pass". A score of 30 skips the only assignment, so result never gets a value. The fix is to set a default before the branch (result = "fail") or add an else. The fixes run above prints label: pass fail.
A close cousin of this bug is a function that only returns inside an if. That one does not raise UnboundLocalError. It quietly returns None, and the crash shows up later as 'NoneType' object is not subscriptable.
4. Using a loop variable after an empty loop
A for loop assigns its target on every pass. If the iterable is empty, the loop body never runs and the name never gets a value:
def last_item(items): for item in items: pass return item print(last_item(["a", "b"])) print(last_item([])) b Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c04_empty_loop.py", line 7, in <module> print(last_item([])) ~~~~~~~~~^^^^ File "/workspace/seo-posts-nov08/test/python/c04_empty_loop.py", line 4, in last_item return item ^^^^ UnboundLocalError: cannot access local variable 'item' where it is not associated with a value Two items work fine. An empty list crashes. Give the name a value before the loop, as last_item(items, default=None) does in the fixes run, or skip the loop entirely and use items[-1] if items else None. Empty lists cause plenty of other crashes too. If you also index into them, see IndexError: list index out of range.
5. A try block that failed before the assignment
This is a nasty one, because the second error hides the first. open() fails, so f is never assigned, and then finally tries to close it:
def load(path): try: f = open(path) data = f.read() except FileNotFoundError: print("missing file:", path) finally: f.close() load("does-not-exist.txt") missing file: does-not-exist.txt Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c05_try_except.py", line 10, in <module> load("does-not-exist.txt") ~~~~^^^^^^^^^^^^^^^^^^^^^^ File "/workspace/seo-posts-nov08/test/python/c05_try_except.py", line 8, in load f.close() ^ UnboundLocalError: cannot access local variable 'f' where it is not associated with a value The handler ran and printed its message, then f.close() blew up. For files, sockets and locks, a with block solves this completely: it only closes what actually opened. When you cannot use with, set the variable to None before the try and check it in finally. Here are the with version and the fixes for causes 9 and 10:
# Fix: with closes the file only if it opened def load(path): try: with open(path) as f: return f.read() except FileNotFoundError: print("missing file:", path) return None print("load:", load("does-not-exist.txt")) # Fix: keep the exception in another name def parse(value): last_error = None try: return int(value) except ValueError as err: last_error = err print("last error was:", repr(last_error)) parse("abc") # Fix: rename instead of shadowing a builtin def summary(words): print("count:", len(words)) word_count = len(words) return word_count print("summary:", summary(["hi", "there"])) missing file: does-not-exist.txt load: None last error was: ValueError("invalid literal for int() with base 10: 'abc'") count: 2 summary: 2 6. A nested function updating an outer variable
Closures follow the same rule. The inner function assigns count, so count is local to the inner function, even though the outer function already has one:
def make_counter(): count = 0 def increment(): count += 1 return count return increment counter = make_counter() print(counter()) Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c06_nested.py", line 9, in <module> print(counter()) ~~~~~~~^^ File "/workspace/seo-posts-nov08/test/python/c06_nested.py", line 4, in increment count += 1 ^^^^^ UnboundLocalError: cannot access local variable 'count' where it is not associated with a value The fix is nonlocal, which tells Python to bind the name in the nearest enclosing function scope. In the fixes run, the counter with nonlocal n printed nonlocal: 1 2 3. The official Python programming FAQ covers this exact case under "Why am I getting an UnboundLocalError when the variable has a value?".
global would be wrong here: it points at the module, not at make_counter.
7. Reading a name after del
del unbinds a name. Inside a function, the name stays local, so reading it afterwards raises the same error:
def clean(): temp = [1, 2, 3] del temp return len(temp) clean() Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c07_del.py", line 6, in <module> clean() ~~~~~^^ File "/workspace/seo-posts-nov08/test/python/c07_del.py", line 4, in clean return len(temp) ^^^^ UnboundLocalError: cannot access local variable 'temp' where it is not associated with a value This usually appears after a refactor adds a del above an older use. Move the del after the last use, or drop it; locals are freed when the function returns anyway.
8. An import later in the same function
import json is an assignment too: it binds the name json. Put one inside a function, even in a branch that never runs, and the module-level import stops working for that whole function:
import json def save(data): text = json.dumps(data) if not text: import json return text print(save({"a": 1})) Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c08_late_import.py", line 9, in <module> print(save({"a": 1})) ~~~~^^^^^^^^^^ File "/workspace/seo-posts-nov08/test/python/c08_late_import.py", line 4, in save text = json.dumps(data) ^^^^ UnboundLocalError: cannot access local variable 'json' where it is not associated with a value The same thing happens with from x import y and with any name that matches a module you imported at the top. Keep imports at the top of the file, or put a lazy import at the very start of the function.
9. Using an except ... as variable after the block
Python deletes the name bound by except ... as err when the handler finishes. The compound statements reference explains why: it breaks a reference cycle between the exception and the stack frame. Inside a function, reading err afterwards fails:
def parse(value): try: return int(value) except ValueError as err: print("bad value:", value) print("last error was:", err) parse("abc") bad value: abc Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c09_except_as.py", line 8, in <module> parse("abc") ~~~~~^^^^^^^ File "/workspace/seo-posts-nov08/test/python/c09_except_as.py", line 6, in parse print("last error was:", err) ^^^ UnboundLocalError: cannot access local variable 'err' where it is not associated with a value At module level the same code raises NameError instead. Either way, copy the exception to another variable inside the handler (last_error = err), as the fixes run above shows. It printed last error was: ValueError("invalid literal for int() with base 10: 'abc'").
10. Shadowing a built-in like len
Built-ins follow the rule as well. Assign to len, list, id, type or sum anywhere in a function, and every earlier use of that built-in in the function breaks:
def summary(words): print("count:", len(words)) len = 3 return len summary(["hi", "there"]) Traceback (most recent call last): File "/workspace/seo-posts-nov08/test/python/c10_shadow_builtin.py", line 6, in <module> summary(["hi", "there"]) ~~~~~~~^^^^^^^^^^^^^^^^^ File "/workspace/seo-posts-nov08/test/python/c10_shadow_builtin.py", line 2, in summary print("count:", len(words)) ^^^ UnboundLocalError: cannot access local variable 'len' where it is not associated with a value The error says "local variable 'len'", which looks bizarre until you spot the assignment two lines down. Rename the variable (word_count) and the built-in works again. If you are cleaning up text or JSON while you debug, my free browser tools run entirely on your device. Pylint (redefined-builtin) and Ruff with its flake8-builtins rules flag shadowed built-ins before you run the code.
How to find the cause in under a minute
- Read the last line of the traceback and note the variable name.
- Search the whole function for that name followed by
=,+=,for,import,del,asor a walrus:=. - If the assignment is meant to change an outer variable, add
global(module) ornonlocal(enclosing function). - If it is a different value with the same name, rename it.
- If it only happens on some inputs, look for an
if, loop ortrythat can skip the assignment, and give the variable a default first. - Still stuck? Print
your_function.__code__.co_varnames, or use the symtable module as in the diagnosis script above.
Frequently asked questions
What does UnboundLocalError: cannot access local variable mean?
Python decided at compile time that the name is local to the function, because something in the function assigns to it. Then a line read the name before that assignment had run, so the local had no value.
Is it the same as local variable referenced before assignment?
Yes. Python 3.10 and older print local variable 'x' referenced before assignment. Python 3.11 and newer print cannot access local variable 'x' where it is not associated with a value. In my tests the cause and fixes were identical.
Why does the error happen when the variable is defined at the top of the file?
Any assignment to the same name inside the function, including +=, makes it a separate local variable for the whole function. The module-level variable is ignored until you declare it with global.
Should I just use global to fix it?
Use global only when the function genuinely must change module-level state. In most code it is cleaner to pass the value in as an argument and return the new value.
What is the difference between global and nonlocal?
global binds the name at module level. nonlocal binds it in the nearest enclosing function, which is what you need for counters inside closures and decorators.
Is UnboundLocalError a subclass of NameError?
Yes. issubclass(UnboundLocalError, NameError) returned True in my test, so except NameError catches both. Fixing the scope is still better than catching the error.
Last checked: 8 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.
Originally published on mfaysal.com.
I'm learning cybersecurity on TryHackMe — follow my progress at tryhackme.com/p/mfaysal.dev.
