When you encounter an UnboundLocalError local variable in Python, it means you're referencing a variable before assigning it within the current function scope. It's one of the most confusing gotchas in Python — your code looks correct, the variable exists elsewhere, yet the interpreter throws an error. The culprit is Python's scoping rules: the language decides at parse time whether a name is local or global, based solely on whether an assignment to that name appears anywhere in the function body.
This error is high in dev forums and Stack Overflow because it violates intuition from other languages. Unlike JavaScript or Ruby, Python doesn't wait until runtime to decide scope — it scans the entire function first. If it sees x = ... anywhere in the function, every reference to x is treated as a local variable. If you reference x before that assignment, you get UnboundLocalError.
What UnboundLocalError actually means
UnboundLocalError occurs when Python has identified a name as local to your function, but you try to use it before that local variable has been assigned a value. This is a runtime error, but the reason it happens is a compile-time scoping decision.
Here's the simplest example:
x = "global value"
def my_func():
print(x) # UnboundLocalError: local variable 'x' referenced before assignment
x = "local value"
my_func()
When Python parses my_func(), it sees x = "local value" on line 4. That assignment tells Python: "x is a local variable in this function." So when line 3 tries to print x, Python looks for it in the local scope — and it's not there yet. Hence the error.
The global x is completely ignored, because Python has already decided that x is local within my_func().
Why Python scopes this way
Python uses this "scan first, execute later" approach because it makes the language faster and more predictable. Every reference to a variable can be compiled to direct memory access, rather than a runtime lookup table. This design choice is baked into the language — you can't change it.
The upside: once you understand the rule, you can spot scope bugs without running the code. The downside: beginners (and tired developers) get caught off guard.
The four scoping solutions
1. Use a different variable name (simplest fix)
If you need both a global and local version, rename one:
global_count = 0
def increment():
local_count = global_count + 1
print(local_count)
increment() # Prints: 1
This avoids the scope confusion entirely and is usually the cleanest approach.
2. Read first, then modify (safe refactoring)
Restructure your function so all reads come after the first assignment:
x = 10
def my_func():
x = x + 1 # Still UnboundLocalError!
return x
Wrong. Even here, because x = ... appears in the function, x is local everywhere. Instead:
x = 10
def my_func():
x = 20
x = x + 1 # Now safe — reads the local x assigned above
return x
print(my_func()) # Prints: 21
3. Use global for module-level variables
Declare that you intend to use the global name:
x = 10
def modify_global():
global x
x = x + 1
return x
print(modify_global()) # Prints: 11
print(x) # Prints: 11 (global was modified)
The global keyword tells Python: "I want the global x, not a local one." Now x = x + 1 reads and modifies the global.
Use global cautiously. Global state makes code harder to test and debug. If you're modifying globals in many functions, consider refactoring into a class with instance variables instead.
4. Use nonlocal for enclosing function scope
When you're in a nested function and want to modify a variable from the enclosing function, use nonlocal:
def outer():
count = 0
def inner():
nonlocal count
count += 1
return count
print(inner()) # Prints: 1
print(inner()) # Prints: 2
return count
print(outer()) # Prints: 2
Without nonlocal, count += 1 would treat count as a local variable and throw UnboundLocalError. nonlocal tells Python: "This variable belongs to the enclosing function scope, not this one."
Common real-world patterns that trigger this
Accumulating in a loop:
total = 0
def sum_list(items):
for item in items:
total = total + item # UnboundLocalError
return total
Because total = ... appears in the function, total is local. Solution: use a local variable or pass the accumulator as a parameter.
Conditional assignment:
flag = False
def check(condition):
if condition:
flag = True # Python sees this assignment...
print(flag) # ...so this line treats flag as local!
check(False) # UnboundLocalError when condition is False
Config mutations:
config = {"debug": False}
def enable_debug():
config["debug"] = True # OK — dict mutation, not reassignment
config = {"new": "config"} # But if you reassign config...
# ...then earlier references to config become UnboundLocalError
Catching scope bugs in production
UnboundLocalError is a Python-specific quirk that static analysis tools can catch — but only if your code is actually running. When users hit this in production, how-to-debug-production-errors becomes critical. Error tracking with full stack traces and context lets you see which line the error came from and inspect the function's local scope state.
Integrating a how-to-read-a-stack-trace practice into your team's workflow also helps: the traceback pinpoints the exact line where Python expected a local value but found none.
When debugging UnboundLocalError in production, pay attention to the line number in the traceback. It shows you exactly where the read happened, which makes the scope problem obvious.
Preventing scope confusion
Use type hints and linters. Tools like Pylint and Flake8 can warn about uninitialized variables. MyPy catches some scope issues too.
def my_func(x: int) -> int:
return x + 1 # x is a parameter, always in scope
Prefer function parameters and return values. They're explicit and testable:
def increment(value):
return value + 1
result = increment(10) # Clear, no scope confusion
Avoid modifying globals and closures. If you need mutable state, wrap it in a class or pass it as a parameter.
Keep functions small. Shorter functions have fewer variables to track. If a function is doing too much, scope bugs are a symptom of a larger design issue.
Best practices summary
- Assign before you read. If a variable is assigned anywhere in a function, it's local everywhere.
- Use
globalornonlocaldeliberately, not by default. They're escape hatches, not the primary pattern. - Prefer function parameters and returns. They're self-documenting and testable.
- Lint and test. Static analysis catches many scope bugs before runtime.
The UnboundLocalError teaches a valuable lesson: understand your language's scoping rules deeply. Once you do, you'll spot these bugs instantly — in code review, in your own functions, and in production traces.
For more systematic debugging of errors, check out error-tracking-best-practices to build a workflow that catches scope bugs and other silent failures before they reach users.
Start tracking errors in minutes
Start tracking Python errors end-to-end with LightTrace. See stack traces, local variables, and full context — then fix scope bugs faster. Try LightTrace free.