How to Access Variables defined outside of Scope in Python
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():
# ✅ Correct: Reading works automatically
print(f"Reading global x: {x}")
read_global()
Output:
Reading global x: 10
Method 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:
# ⛔️ Incorrect: Python treats 'counter' as local because we are assigning to it
counter += 1
except UnboundLocalError as e:
print(f"Error: {e}")
def increment_success():
# ✅ Correct: Explicitly state that 'counter' refers to the global variable
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: 1
Minimize the use of global variables. They make code harder to debug and test because functions become dependent on the external state of the script.
Method 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():
# ⛔️ Incorrect: Creates a new local variable 'count' instead of modifying the outer one
# or raises UnboundLocalError depending on usage
count = 10
def inner_success():
nonlocal count
# ✅ Correct: Modifies the variable in 'outer_function'
count += 5
inner_success()
print(f"Outer count value: {count}")
outer_function()
Output:
Outer count value: 5
nonlocal does not allow you to access Global variables; it only climbs up to the nearest Enclosing function scope.
Method 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):
# This inner function 'captures' the 'factor' variable
def multiplier(x):
return x * factor
return multiplier
# Create specific versions of the function
double = create_multiplier(2)
triple = create_multiplier(3)
# ✅ Correct: accessing the 'factor' variable captured during creation
print(f"Double 5: {double(5)}")
print(f"Triple 5: {triple(5)}")
Output:
Double 5: 10
Triple 5: 15
Common 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.
# ⛔️ Incorrect: The list 'lst' persists across calls
def append_bad(value, lst=[]):
lst.append(value)
return lst
print(append_bad(1))
print(append_bad(2)) # Unexpected: [1, 2]
# ✅ Correct: Use None as a sentinel value
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.