Python - Global and Local Variables (Scope)
Python resolves variable names using the LEGB rule: Local, Enclosing, Global, and Built-in scopes. When you reference a variable, Python searches these scopes in order until it finds the name.
Key Insights
- Python’s LEGB rule (Local, Enclosing, Global, Built-in) determines variable resolution order, with local scope taking precedence over outer scopes
- The
globalandnonlocalkeywords allow modification of variables outside the current scope, but overuse creates hard-to-maintain code - Function parameters, loop variables, and comprehensions create their own local scopes, which can lead to unexpected behavior if scope rules aren’t understood
Understanding Python’s Scope Hierarchy
Python resolves variable names using the LEGB rule: Local, Enclosing, Global, and Built-in scopes. When you reference a variable, Python searches these scopes in order until it finds the name.
x = "global" # Global scope
def outer():
x = "enclosing" # Enclosing scope
def inner():
x = "local" # Local scope
print(x)
inner()
print(x)
outer()
print(x)
# Output:
# local
# enclosing
# global
Built-in scope contains Python’s built-in functions like len(), print(), and range(). You can access these from anywhere without importing them.
def demonstrate_builtin():
# 'len' is in built-in scope
result = len([1, 2, 3])
return result
print(demonstrate_builtin()) # 3
Local Scope Behavior
Variables defined inside a function exist only within that function’s local scope. They’re created when the function executes and destroyed when it returns.
def calculate_total(items):
total = 0 # Local variable
for item in items:
total += item
return total
result = calculate_total([10, 20, 30])
print(result) # 60
# This raises NameError: name 'total' is not defined
# print(total)
Function parameters are also local variables:
def greet(name, greeting="Hello"):
message = f"{greeting}, {name}!" # Both name and greeting are local
return message
print(greet("Alice")) # Hello, Alice!
# print(name) # NameError
The Global Keyword
To modify a global variable from within a function, use the global keyword. Without it, Python creates a new local variable instead.
counter = 0 # Global variable
def increment_wrong():
counter = counter + 1 # UnboundLocalError!
return counter
def increment_correct():
global counter
counter = counter + 1
return counter
# increment_wrong() # Raises UnboundLocalError
increment_correct()
print(counter) # 1
The UnboundLocalError occurs because Python sees the assignment and treats counter as local, but then tries to read it before assignment.
Here’s a practical example with multiple global variables:
request_count = 0
error_count = 0
def process_request(success):
global request_count, error_count
request_count += 1
if not success:
error_count += 1
process_request(True)
process_request(False)
process_request(True)
print(f"Requests: {request_count}, Errors: {error_count}")
# Requests: 3, Errors: 1
The Nonlocal Keyword
The nonlocal keyword modifies variables in the nearest enclosing scope, useful for nested functions and closures.
def outer():
count = 0
def increment():
nonlocal count
count += 1
return count
print(increment()) # 1
print(increment()) # 2
print(increment()) # 3
outer()
Without nonlocal, you’d get an UnboundLocalError:
def outer():
count = 0
def increment_wrong():
count = count + 1 # UnboundLocalError
return count
increment_wrong()
Practical use case - creating a closure with state:
def create_multiplier(factor):
def multiply(number):
nonlocal factor
result = number * factor
factor += 1 # Modify enclosing scope variable
return result
return multiply
times_three = create_multiplier(3)
print(times_three(10)) # 30 (factor becomes 4)
print(times_three(10)) # 40 (factor becomes 5)
print(times_three(10)) # 50 (factor becomes 6)
Scope in Comprehensions and Loops
List comprehensions, dict comprehensions, and generator expressions create their own scope in Python 3, but regular loops don’t.
# Comprehension variables are scoped
numbers = [1, 2, 3, 4, 5]
squares = [x**2 for x in numbers]
# print(x) # NameError in Python 3
# Loop variables leak into enclosing scope
for i in range(3):
pass
print(i) # 2 (variable still exists)
This difference matters when nesting comprehensions:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Each comprehension has its own scope for iteration variables
flattened = [num for row in matrix for num in row]
print(flattened) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Common Pitfalls and Solutions
Pitfall 1: Late binding in closures
# Problem: all functions reference the same 'i'
functions = []
for i in range(3):
functions.append(lambda: i)
for f in functions:
print(f()) # Prints: 2, 2, 2 (not 0, 1, 2)
# Solution: use default argument to capture current value
functions = []
for i in range(3):
functions.append(lambda x=i: x)
for f in functions:
print(f()) # Prints: 0, 1, 2
Pitfall 2: Shadowing built-ins
# Avoid shadowing built-in names
list = [1, 2, 3] # Bad: shadows built-in list()
print(list([4, 5])) # TypeError: 'list' object is not callable
# Use different names
items = [1, 2, 3] # Good
print(list([4, 5])) # Works fine
Pitfall 3: Mutable default arguments
# Problem: default list is shared across calls
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] - unexpected!
# Solution: use None as default
def add_item_correct(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item_correct(1)) # [1]
print(add_item_correct(2)) # [2] - correct!
Inspecting Scope with Built-in Functions
Python provides functions to examine variable scopes:
global_var = "global"
def inspect_scope():
local_var = "local"
print("Local variables:", locals())
print("Global variables:", list(globals().keys())[:5]) # First 5
inspect_scope()
# Output shows dictionaries of local and global namespaces
Use vars() to get the namespace dictionary:
class Config:
debug = True
timeout = 30
print(vars(Config))
# {'__module__': '__main__', 'debug': True, 'timeout': 30, ...}
Best Practices
Minimize global variable usage. Instead, use function parameters and return values:
# Avoid this
total = 0
def add_to_total(value):
global total
total += value
# Prefer this
def add_to_total(current_total, value):
return current_total + value
total = 0
total = add_to_total(total, 10)
Use classes to manage state instead of global variables:
class RequestTracker:
def __init__(self):
self.request_count = 0
self.error_count = 0
def process_request(self, success):
self.request_count += 1
if not success:
self.error_count += 1
def get_stats(self):
return {
'requests': self.request_count,
'errors': self.error_count
}
tracker = RequestTracker()
tracker.process_request(True)
tracker.process_request(False)
print(tracker.get_stats()) # {'requests': 2, 'errors': 1}
Understanding Python’s scoping rules prevents bugs and makes code more maintainable. Use global and nonlocal sparingly, prefer explicit parameter passing, and leverage classes for state management.