In Python, accessing variables defined outside of the current function (external variables) depends entirely on Scope. While Python allows you to read global or enclosing variables easily, modifying them requires specific keywords and an understanding of the LEGB rule (Local, Enclosing, Global, Built-in).
This guide explains how to properly access and modify external variables using global, nonlocal, and closures.
Understanding the Scope Hierarchy (LEGB)
Python resolves variables in a specific order known as LEGB:
- Local: Inside the current function.
- Enclosing: Inside enclosing functions (if nested).
- Global: At the top level of the module.
- Built-in: Predefined Python keywords (e.g.,
len,print).
You can read a global variable inside a function without any special syntax, but you cannot modify it without declaring your intent.
x = 10 # Global variable
def read_global():
print(f"Reading global x: {x}")
read_global()Output:
Reading global x: 10Method 1: Modifying Module-Level Variables (global)
If you attempt to modify a global variable inside a function (e.g., x += 1), Python assumes x is a local variable that hasn't been defined yet, raising an UnboundLocalError. To fix this, use the global keyword.
counter = 0 # Global variable
def increment_fail():
try:
counter += 1
except UnboundLocalError as e:
print(f"Error: {e}")
def increment_success():
global counter
counter += 1
print(f"Counter is now: {counter}")
increment_fail()
increment_success()Output:
Error: local variable 'counter' referenced before assignment
Counter is now: 1Method 2: Modifying Nested Variables (nonlocal)
When working with nested functions (a function inside another function), the inner function can access variables from the outer function (Enclosing scope). To modify them, use the nonlocal keyword.
def outer_function():
count = 0 # Enclosing variable (Local to outer, Nonlocal to inner)
def inner_fail():
count = 10
def inner_success():
nonlocal count
count += 5
inner_success()
print(f"Outer count value: {count}")
outer_function()Output:
Outer count value: 5Method 3: Using Closures (State Retention)
A cleaner alternative to using global variables for maintaining state (like a counter) is using a Closure. A closure allows a function to "remember" the environment in which it was created.
def create_multiplier(factor):
def multiplier(x):
return x * factor
return multiplier
double = create_multiplier(2)
triple = create_multiplier(3)
print(f"Double 5: {double(5)}")
print(f"Triple 5: {triple(5)}")Output:
Double 5: 10
Triple 5: 15Common Pitfall: Mutable Default Arguments
Sometimes, external variables aren't the issue, but rather how Python handles default arguments. Default arguments are evaluated only once when the function is defined, not every time it is called. This creates a persistent variable across calls.
def append_bad(value, lst=[]):
lst.append(value)
return lst
print(append_bad(1))
print(append_bad(2)) # Unexpected: [1, 2]
def append_good(value, lst=None):
if lst is None:
lst = [] # Create a new local list every time
lst.append(value)
return lst
print(append_good(1))
print(append_good(2)) # Expected: [2]Output:
[1]
[1, 2]
[1]
[2]Conclusion
To manage variable access effectively in Python:
- Reading: You can read Global or Enclosing variables directly without special syntax.
- Writing Global: Use the
globalkeyword to modify variables defined at the module level. - Writing Enclosing: Use the
nonlocalkeyword to modify variables defined in an outer function. - Best Practice: Prefer passing arguments and returning values or using Closures over relying heavily on global state modifications.