As a Python developer, chances are you have encountered the confusing UnboundLocalError
at some point. This error happens when you try to access a local variable in a function or method, but Python cannot find the variable defined in the local scope.
The UnboundLocalError
can be frustrating for beginners, leaving them scratching their heads wondering what went wrong. However, once you understand the causes behind this error, it becomes much easier to prevent and fix.
In this comprehensive guide, we will demystify Python‘s UnboundLocalError by covering:
- What triggers the UnboundLocalError and common causes
- Actionable solutions and best practices to avoid the error
- Real code examples to demonstrate the error and fixes
- Additional tips and tricks for using local and global variables correctly
So let‘s get started unraveling the UnboundLocalError
!
What Triggers An UnboundLocalError in Python?
In Python, variables defined within a function are considered local variables, meaning they only exist within the local scope of that function. Trying to use a local variable that hasn‘t been defined causes an UnboundLocalError
.
Here is an example that demonstrates this:
def my_func():
print(local_var)
my_func()
Running this code produces:
UnboundLocalError: local variable ‘local_var‘ referenced before assignment
Python is telling us that local_var
doesn‘t exist within the local scope of my_func()
. We tried to print local_var
before defining it, resulting in the error.
In addition to accessing undefined local variables, there are a couple other common triggers for UnboundLocalError
in Python:
Attempting to modify a global variable:
global_var = "Hello"
def my_func():
global_var = "World"
print(global_var)
my_func()
This will produce an UnboundLocalError
on global_var
within my_func()
, even though global_var
is defined globally. Python sees we are assigning to global_var
inside the function and treats it as a local variable, but global_var
isn‘t defined locally which causes issues.
Closing over a variable in nested functions:
def outer_func():
x = 120
def inner_func():
print(x)
inner_func()
outer_func()
This example also causes an UnboundLocalError
, but on x
inside inner_func()
. Python doesn‘t automatically make x
from outer_func()
available to nested functions. We have to handle closing over variables more carefully as we‘ll discuss next.
There are solutions for both of these scenarios, but first it‘s useful to learn more about Python‘s variable scoping rules that are the root causes of these issues.
Understanding Python‘s Variable Scope Rules
The key to avoiding UnboundLocalError
is learning Python‘s LEGB rule for variable scoping along with best practices for using global and nested local variables.
Python‘s LEGB Variable Scope Rule
Python resolves variables in this order:
- L: Local — Variables defined within function or method
- E: Enclosing — Variables defined in any enclosing functions
- G: Global — Variables defined at the uppermost level
- B: Built-in — Built-in functions and exceptions
So when accessing variables:
- Python first checks for a local variable defined within the current function or method
- Then searches any enclosing functions from inner to outer
- If no local variable is found, searches the global variables
- And lastly checks built-ins
Many UnboundLocalError
cases happen due to confusion between local vs global variables in Python. Keeping these precedence rules in mind helps avoid issues.
Best Practices for Local and Global Variables
Here are some key best practices for properly using local and global variables in Python:
- Declare globals needed inside functions: If wanting to modify globals within a function, declare them using
global
to avoid confusion
global_var = "Hello"
def my_func():
global global_var
global_var = "Hi" # Ok now
- Access values, modify locally: Avoid modifying globals from within functions when possible. Access values, make local copy to modify
config = {"setting": True}
def toggle_setting():
setting = config["setting"] # Access
setting = not setting # Modify local
- Return values from functions rather than relying on side effects
def double(x):
return x * 2 # Return new value
x = 5
print(double(x)) # Side effect free
- Use parameters and returns instead of external state: Rely on parameter passing between functions rather than globals
def double(x):
return x * 2
x = 5
x2 = double(x) # Explicit data in/out
Keeping these variable usage best practices helps avoid confusing Python‘s LEGB scoping rules, preventing many UnboundLocalError
cases.
Now let‘s look at solutions for the specific variable scoping scenarios we saw earlier.
Solutions and Best Practices for Avoiding UnboundLocalError
While sound variable usage recommendations help avoid UnboundLocalError
in general, you may still run into tricky situations where Python‘s scoping trips you up. Here are some pro tips for resolving two common specific cases.
Modifying Global Variables
Earlier we saw code for modifying a global variable within a function that failed:
global_var = "Hello"
def my_func():
global_var = "Hi" # UnboundLocalError!
print(global_var)
The issue is Python treats global_var
as a local variable since we assign to it. The solution is using the global
keyword:
global_var = "Hello"
def my_func():
global global_var # Declare global
global_var = "Hi" # Now works!
print(global_var)
By using global
we tell Python global_var
is defined externally, fixing the scoping issue.
A better practice is avoiding modifications entirely though:
config = {"setting": True}
def toggle_setting():
setting = config["setting"]
setting = not setting
return setting
updated_setting = toggle_setting()
Retrieve global values into local variables, modify locally, return new copies. This results in cleaner code.
Closing Over Variables in Nested Functions
We also saw an UnboundLocalError
example with nested functions:
def outer_func():
x = 120
def inner_func():
print(x) # UnboundLocalError
inner_func()
The problem here is that Python doesn‘t automatically make outer_func()
‘s x
variable available to reference within inner_func()
.
We can fix this by manually closing over x
:
def outer_func():
x = 120
def inner_func(x): # Pass x as parameter
print(x)
inner_func(x) # Supply reference explicitly
outer_func()
Here we pass x
as a parameter to make the binding available in inner_func()
‘s scope.
An alternative approach is using a nonlocal
declaration:
def outer_func():
x = 120
def inner_func():
nonlocal x
print(x)
inner_func()
The nonlocal
keyword tells Python to resolve x
as a variable from an enclosing function scope. Similar to global
but for outer function scopes rather than global.
For better encapsulation, instead of relying on external state through closures, have inner functions accept parameters and return updated values explicitly.
Additional Tips For Properly Using Variables in Python
Here are a few closing tips when leveraging local, enclosing, and global variables in Python:
Use descriptive variable names: Well-named local, global, and nonlocal variables help clarify your code and reduce confusion that leads to UnboundLocalError
.
Limit variable scope when reasonable: Keep variables local to the narrowest reasonable scope, and lift to a wider scope only when needed. This reduces likelihood of accidental overlap.
Modularize code using functions: Break code into discrete reusable functions rather than relying on shared global state. Functions encourage scoping variables appropriately.
Leverage linters and type checkers: Tools like pylance
, mypy
, pylint
can help analyze code and flag issues early around improper variable usage which could lead to UnboundLocalError
.
Putting some discipline around properly scoping variables this way will help you write clean, modular Python code and squashes UnboundLocalError
for good!
Putting It All Together: Fixing a Real UnboundLocalError Example
Let‘s bring all our new knowledge together to fix a real UnboundLocalError
case.
Here is some flawed code for a simple ecommerce store application:
cart = [] # Global cart
def add_item(item):
cart.append(item) # Add to cart
def view_cart():
print(cart) # Print cart contents
add_item("Apples")
view_cart()
This results in:
UnboundLocalError: local variable ‘cart‘ referenced before assignment
Why the error? Within add_item()
Python sees we are modifying cart
and treats it as a local variable. But cart
isn‘t defined locally in the function causing failure.
We could fix this by using global
to declare cart
:
cart = []
def add_item(item):
global cart
cart.append(item)
def view_cart():
print(cart)
add_item("Oranges")
view_cart() # [‘Oranges‘]
However, relying on the shared global cart is poor practice and couples our functions. Instead, we refactor to explicitly pass cart state between functions:
def add_item(cart, item):
cart.append(item)
return cart
def view_cart(cart):
print(cart)
my_cart = []
my_cart = add_item(my_cart, "Apples")
view_cart(my_cart) # [‘Apples‘] Cleaner!
By eliminating the global variable dependency we decouple things, encourage better scoping practices, and resolve the error.
Putting the major points we covered to work by properly scoping variables and passing state explicitly makes debugging UnboundLocalErrors much easier!
Key Takeaways: Effortlessly Avoiding UnboundLocalError
We have covered a lot of ground demystifying Python‘s UnboundLocalError
. Let‘s review the key takeaways:
UnboundLocalError
happens when trying to access undefined local variables within a function- Also occurs attempting to modify globals or closing over external state improperly
- Understanding Python‘s LEGB variable precedence rule helps avoid issues
- Use best practices like avoiding globals, modularizing appropriately, descriptive names, tools
- Declare
global
andnonlocal
where needed for specific situations - Pass data between functions explicitly rather than relying on shared state
Learning these variable scoping best practices and solutions for common UnboundLocalError
cases will help you become a debugging pro in no time. You are now equipped to leverage local, global, and nested function variables with confidence!
With your new skills, the confusing UnboundLocalError
will transform from a source of frustration to an easy fix. So get out there, write some Python code, and put your new understanding to work in squashing this error for good!