21. Variable Scope and Name Resolution
When you create a variable in Python, where does it "live"? Can a function see variables created outside it? Can code outside a function access variables created inside it? These questions are about scope - the region of your program where a name is visible and can be used.
Understanding scope is crucial for writing functions that work correctly and predictably. Without this knowledge, you might accidentally create bugs where variables don't have the values you expect, or where changes to variables don't persist as intended.
In this chapter, we'll explore how Python determines which variable a name refers to, how to control where variables are accessible, and what happens when you delete a name. By the end, you'll understand the rules that govern variable visibility in Python programs.
21.1) Local and Global Variables
Every variable in Python exists within a specific scope - a region of code where that variable name is defined and accessible. The two most fundamental scopes are local and global.
Understanding Global Scope
Variables created at the top level of your program - outside any function - exist in the global scope. These are called global variables, and they're accessible from anywhere in your module after they're defined.
# Global variable - defined at module level
total_users = 0
def show_user_count():
# This function can READ the global variable
print(f"Total users: {total_users}")
show_user_count() # Output: Total users: 0
print(total_users) # Output: 0In this example, total_users is a global variable. Both the function show_user_count() and the code at module level can access it. Think of global variables as being visible throughout your entire program file.
Understanding Local Scope
Variables created inside a function exist in that function's local scope. These are called local variables, and they're only accessible within the function where they're defined. Once the function finishes executing, local variables disappear.
def calculate_discount(price):
# discount_rate is LOCAL to this function
discount_rate = 0.15
discount_amount = price * discount_rate
return discount_amount
result = calculate_discount(100)
print(result) # Output: 15.0
# This would cause an error - discount_rate doesn't exist here
# print(discount_rate) # NameError: name 'discount_rate' is not definedThe variables discount_rate and discount_amount exist only while calculate_discount() is running. After the function returns, these names no longer exist. This is actually a good thing - it prevents functions from cluttering your program with temporary variables.
Why Local Scope Matters
Local scope provides encapsulation - each function has its own private workspace. This means you can use the same variable names in different functions without conflicts:
def calculate_tax(amount):
rate = 0.08 # Local variable
return amount * rate
def calculate_shipping(weight):
rate = 5.00 # Different local variable with the same name
return weight * rate
tax = calculate_tax(100)
shipping = calculate_shipping(3)
print(f"Tax: ${tax}") # Output: Tax: $8.0
print(f"Shipping: ${shipping}") # Output: Shipping: $15.0Both functions use a variable named rate, but they're completely separate variables in different local scopes. Changes to rate in one function don't affect rate in the other function. This isolation makes functions more reliable and easier to understand.
Reading Global Variables from Functions
Functions can read global variables without any special syntax:
# Global configuration
max_login_attempts = 3
def check_login(password):
# Reading global variable
if password == "secret123":
return "Login successful"
else:
return f"Invalid password. You have {max_login_attempts} attempts."
result = check_login("wrong")
print(result) # Output: Invalid password. You have 3 attempts.The function check_login() can read max_login_attempts because it's a global variable. However, there's an important limitation we need to understand.
The Assignment Creates Local Variables Rule
Here's where scope gets tricky. If you assign to a variable name inside a function, Python creates a new local variable with that name, even if a global variable with the same name exists:
counter = 0 # Global variable
def increment_counter():
# WARNING: This creates a NEW local variable named counter - for demonstration only
# PROBLEM: Trying to read counter before assigning to it locally
counter = counter + 1 # UnboundLocalError: local variable 'counter' referenced before assignment
print(counter)
# increment_counter() # This call results in UnboundLocalErrorThis code fails because Python sees the assignment counter = counter + 1 and decides that counter must be a local variable. But then when it tries to evaluate counter + 1, the local variable counter doesn't have a value yet - we're trying to use it before we've assigned to it.
This is a common source of confusion. The rule is: if a function assigns to a variable name anywhere in its body, that name is treated as local throughout the entire function, even before the assignment.
Let's see this more clearly:
message = "Hello" # Global variable
def show_message():
print(message) # This works - just reading the global
def change_message():
# WARNING: This demonstrates a common error - for demonstration only
# PROBLEM: Python sees assignment below, so message is treated as local throughout
print(message) # UnboundLocalError!
message = "Goodbye" # This makes message local for the ENTIRE function
show_message() # Output: Hello
# change_message() # This call results in UnboundLocalErrorThe function show_message() works fine because it only reads message. But change_message() fails because the assignment on the second line makes Python treat message as local throughout the entire function, including the print() statement that comes before the assignment.
Parameters Are Local Variables
Function parameters are local variables that get their initial values from the arguments passed when the function is called:
def greet(name): # 'name' is a local variable
greeting = f"Hello, {name}!" # 'greeting' is also local
return greeting
message = greet("Alice")
print(message) # Output: Hello, Alice!
# Neither 'name' nor 'greeting' exist here
# print(name) # NameErrorThe parameter name exists only within the greet() function. It's created when the function is called and disappears when the function returns.
Practical Example: Shopping Cart Calculation
Let's see how local and global scope work together in a realistic scenario:
# Global configuration
tax_rate = 0.08
free_shipping_threshold = 50
def calculate_total(subtotal):
# Local variables for this calculation
tax = subtotal * tax_rate # Reading global tax_rate
# Determine shipping cost
if subtotal >= free_shipping_threshold: # Reading global threshold
shipping = 0
else:
shipping = 5.99
total = subtotal + tax + shipping
return total
# Calculate for different cart values
cart1 = calculate_total(30)
cart2 = calculate_total(60)
print(f"Cart 1 total: ${cart1:.2f}") # Output: Cart 1 total: $38.39
print(f"Cart 2 total: ${cart2:.2f}") # Output: Cart 2 total: $64.80In this example:
tax_rateandfree_shipping_thresholdare global configuration valuessubtotal,tax,shipping, andtotalare local to each call ofcalculate_total()- Each function call gets its own separate set of local variables
- The function can read the global configuration but doesn't modify it
This separation of concerns makes the code clear: global variables hold configuration that applies everywhere, while local variables hold temporary calculation results specific to each function call.
21.2) The LEGB Rule for Name Resolution
When Python encounters a variable name, how does it know which variable you're referring to? Python follows a specific search order called the LEGB rule. LEGB stands for Local, Enclosing, Global, Built-in - the four scopes Python searches, in that order.
The Four Scopes in LEGB
Let's understand each scope in the LEGB hierarchy:
- Local (L): The current function's scope
- Enclosing (E): The scope of any enclosing functions (functions that contain the current function)
- Global (G): The module-level scope
- Built-in (B): Python's built-in names like
print,len,int, etc.
When you use a variable name, Python searches these scopes in order: L → E → G → B. It uses the first match it finds and stops searching.
Local Scope: The First Place Python Looks
Python always checks the local scope first:
def calculate_price():
price = 100 # Local variable
tax = 0.08 # Local variable
total = price * (1 + tax)
return total
result = calculate_price()
print(result) # Output: 108.0When Python sees price, tax, and total inside calculate_price(), it finds them in the local scope and uses those values. The search stops at the local scope - Python doesn't need to look further.
Global Scope: When Local Doesn't Have It
If a name isn't found locally, Python checks the global scope:
# Global variables
default_tax_rate = 0.08
default_currency = "USD"
def calculate_price(amount):
# 'amount' is local, found immediately
# 'default_tax_rate' is not local, found in global scope
total = amount * (1 + default_tax_rate)
return total
result = calculate_price(100)
print(result) # Output: 108.0When Python encounters default_tax_rate inside the function, it doesn't find it locally, so it searches the global scope and finds it there.
Built-in Scope: Python's Predefined Names
If a name isn't found in local or global scope, Python checks the built-in scope - the names that Python provides automatically:
def process_data(numbers):
# 'numbers' is local
# 'len' is not local or global - it's built-in
count = len(numbers)
# 'max' is also built-in
maximum = max(numbers)
return count, maximum
data = [10, 25, 15, 30, 20]
result = process_data(data)
print(result) # Output: (5, 30)The names len and max aren't defined in your code - they're built-in functions that Python provides. When Python doesn't find these names locally or globally, it checks the built-in scope and finds them there.
Enclosing Scope: Nested Functions
The enclosing scope comes into play when you have nested functions - functions defined inside other functions. This is where the "E" in LEGB becomes important:
def outer_function():
outer_var = "I'm from outer" # In enclosing scope for inner_function
def inner_function():
inner_var = "I'm from inner" # Local to inner_function
# inner_function can see both inner_var (local) and outer_var (enclosing)
print(inner_var) # Output: I'm from inner
print(outer_var) # Output: I'm from outer
inner_function()
outer_function()For inner_function(), the scope of outer_function() is an enclosing scope. When inner_function() references outer_var, Python searches:
- Local scope of
inner_function()- not found - Enclosing scope of
outer_function()- found! Uses this value
LEGB in Action: Simple Example
Let's see all four scopes working together in a clear, straightforward example:
# Built-in: len (Python provides this)
# Global: multiplier
multiplier = 10
def outer(x):
# Enclosing scope for inner
y = 5
def inner(z):
# Local scope
# z is local (L)
# y is from enclosing scope (E)
# multiplier is from global scope (G)
# len is from built-in scope (B)
result = len([z, y, multiplier]) # Uses all four scopes!
return z + y + multiplier
return inner(3)
answer = outer(100)
print(answer) # Output: 18When Python evaluates z + y + multiplier inside inner():
- L (Local): Finds
z = 3 - E (Enclosing): Finds
y = 5inouter() - G (Global): Finds
multiplier = 10 - B (Built-in): Finds
lenfunction
This example clearly demonstrates how Python searches through all four scopes to resolve names.
Shadowing: When Inner Scopes Hide Outer Names
If the same name exists in multiple scopes, the innermost scope "wins" - this is called shadowing:
value = "global"
def outer():
value = "enclosing"
def inner():
value = "local"
print(value) # Which value?
inner()
print(value) # Which value?
outer()
print(value) # Which value?Output:
local
enclosing
globalEach print() statement sees a different value because Python stops at the first match:
- Inside
inner(): findsvaluelocally → prints "local" - Inside
outer()but outsideinner(): findsvalueinouter()'s scope → prints "enclosing" - At module level: finds
valueglobally → prints "global"
Visualizing the LEGB Search Order
This diagram shows Python's search process. It starts at the innermost scope and works outward. If the name isn't found in any scope, Python raises a NameError.
Why LEGB Matters for Writing Functions
Understanding LEGB helps you:
- Predict variable values: You know exactly which variable Python will use
- Avoid naming conflicts: You understand when names shadow each other
- Design better functions: You can decide which scope is appropriate for each variable
- Debug scope issues: When variables don't have expected values, you can trace through LEGB
The LEGB rule is fundamental to how Python resolves names. Every time you use a variable, Python is following this rule behind the scenes.
21.3) Using the global Keyword Carefully
We've seen that functions can read global variables, but what if you need to modify a global variable from inside a function? That's where the global keyword comes in - but it should be used sparingly and carefully.
The Problem: Assignment Creates Local Variables
As we learned earlier, assigning to a variable inside a function creates a local variable:
counter = 0 # Global variable
def increment():
# WARNING: This creates a NEW local variable named counter - for demonstration only
# PROBLEM: Trying to read counter before assigning to it locally
counter = counter + 1 # UnboundLocalError!
# increment() # This call results in UnboundLocalErrorThis fails because Python sees the assignment and treats counter as local throughout the entire function. But we're trying to read counter before we've assigned to it locally.
This is one of the most common errors when working with global variables. The error message UnboundLocalError: local variable 'counter' referenced before assignment tells you exactly what happened: Python decided counter was local (because of the assignment), but you tried to use it before giving it a value.
The Solution: Declaring Variables as Global
The global keyword tells Python: "Don't create a new local variable with this name. Use the global variable instead."
counter = 0 # Global variable
def increment():
global counter # Tell Python to use the global counter
counter = counter + 1 # Now this modifies the global variable
print(f"Before: {counter}") # Output: Before: 0
increment()
print(f"After: {counter}") # Output: After: 1
increment()
print(f"After again: {counter}") # Output: After again: 2The global counter declaration must come before you use the variable. It tells Python that any assignments to counter in this function should modify the global variable, not create a local one.
Multiple Global Variables
You can declare multiple variables as global in one statement:
total_sales = 0
total_customers = 0
def record_sale(amount):
global total_sales, total_customers
total_sales += amount
total_customers += 1
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $0, Customers: 0
record_sale(25.50)
record_sale(30.00)
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $55.5, Customers: 2Both total_sales and total_customers are declared global, so the function can modify both.
When to Use global: Shared State
The global keyword is appropriate when you need to maintain shared state - data that multiple functions need to access and modify:
# Game state
player_score = 0
player_lives = 3
game_over = False
def award_points(points):
global player_score
player_score += points
print(f"Score: {player_score}")
def lose_life():
global player_lives, game_over
player_lives -= 1
print(f"Lives remaining: {player_lives}")
if player_lives <= 0:
game_over = True
print("Game Over!")
def check_game_status():
# Just reading globals - no global keyword needed
if game_over:
return "Game Over"
else:
return f"Playing - Score: {player_score}, Lives: {player_lives}"
# Play the game
award_points(100) # Output: Score: 100
award_points(50) # Output: Score: 150
lose_life() # Output: Lives remaining: 2
print(check_game_status()) # Output: Playing - Score: 150, Lives: 2This example shows appropriate use of global: multiple functions need to modify shared game state. However, notice that check_game_status() doesn't need global because it only reads the variables.
Why global Should Be Used Carefully
While global is sometimes necessary, overusing it can make code harder to understand and maintain. Here's why:
Problem 1: Hidden Dependencies
When functions modify global variables, it's not obvious from the function call what's changing:
total = 0
def add_to_total(value):
global total
total += value
# What does this function do? You can't tell without reading its code
add_to_total(10)Compare this to a function that returns a value:
def add_to_total(current_total, value):
return current_total + value
total = 0
total = add_to_total(total, 10) # Clear: total is being updatedThe second version makes it explicit that total is being modified.
Problem 2: Testing Becomes Harder
Functions that modify global state are harder to test because you need to set up and reset global variables:
# Hard to test - depends on global state
score = 0
def add_score(points):
global score
score += points
# Each test needs to reset score
# Test 1
score = 0
add_score(10)
assert score == 10
# Test 2 - must reset score again
score = 0
add_score(20)
assert score == 20Problem 3: Functions Aren't Reusable
Functions that depend on specific global variables can't be easily reused in other programs:
# This function only works if there's a global variable named 'inventory'
inventory = []
def add_item(item):
global inventory
inventory.append(item)Better Alternatives to global
In many cases, you can avoid global by using return values and parameters:
Instead of modifying global state:
# Using global (less ideal)
balance = 1000
def withdraw(amount):
global balance
if amount <= balance:
balance -= amount
return True
return False
withdraw(100)
print(balance) # Output: 900Use return values:
# Using return values (better)
def withdraw(balance, amount):
if amount <= balance:
return balance - amount, True
return balance, False
balance = 1000
balance, success = withdraw(balance, 100)
print(balance) # Output: 900The second version is more flexible, testable, and reusable.
When global Is Actually Appropriate
There are legitimate uses for global:
- Configuration that truly needs to be global:
# Application-wide settings
debug_mode = False
log_level = "INFO"
def enable_debug():
global debug_mode, log_level
debug_mode = True
log_level = "DEBUG"- Counters for debugging or statistics:
# Track function calls for debugging
_function_call_count = 0
def tracked_function():
global _function_call_count
_function_call_count += 1
# ... rest of functionKey Takeaways About global
- Use
globalonly when you truly need to modify module-level state - Prefer returning values and using parameters instead
- When you do use
global, document why it's necessary - Consider whether your design could be improved to avoid
global - Remember: reading global variables doesn't require the
globalkeyword - only modifying them does
21.4) Using nonlocal to Modify Variables in Enclosing Functions
When you have nested functions, you might need to modify a variable from an enclosing function's scope. The nonlocal keyword serves this purpose - it's like global, but for enclosing function scopes instead of the global scope.
The Problem: Modifying Enclosing Variables
Just as assignment creates local variables by default, the same problem occurs with enclosing scopes:
def outer():
count = 0 # Variable in outer's scope
def inner():
# WARNING: This creates a NEW local variable named count - for demonstration only
# PROBLEM: Trying to read count before assigning to it locally
count = count + 1 # UnboundLocalError!
print(count)
inner()
# outer() # This call results in UnboundLocalErrorPython sees the assignment to count in inner() and treats it as a local variable. But we're trying to read it before assigning to it locally, causing an error.
The Solution: The nonlocal Keyword
The nonlocal keyword tells Python: "This variable isn't local - look for it in the enclosing function's scope and use that one."
def outer():
count = 0 # Variable in outer's scope
def inner():
nonlocal count # Use the count from outer's scope
count = count + 1
print(f"Count in inner: {count}")
print(f"Count before: {count}") # Output: Count before: 0
inner() # Output: Count in inner: 1
print(f"Count after: {count}") # Output: Count after: 1
outer()Now inner() can modify the count variable from outer()'s scope. The change persists after inner() returns because we're modifying the actual variable in the enclosing scope.
Why nonlocal Is Useful: Functions That Remember State
The nonlocal keyword enables a powerful pattern where inner functions can maintain and modify state from their enclosing scope. We'll learn about closures and factory functions in detail in Chapter 23, but for now, understand that nonlocal allows inner functions to modify variables from enclosing scopes.
Here's a simple example showing how nonlocal works:
def create_counter():
count = 0 # This variable is in the enclosing scope for increment
def increment():
nonlocal count # Modify the count from the enclosing scope
count += 1
return count
return increment # Return the inner function
# Create a counter
counter1 = create_counter()
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
# Create another independent counter
counter2 = create_counter()
print(counter2()) # Output: 1
print(counter2()) # Output: 2Each call to create_counter() creates a new count variable and a new increment() function that can modify that specific count using nonlocal.
nonlocal vs global
It's important to understand the difference:
x = "global"
def outer():
x = "enclosing"
def use_global():
global x # Refers to the global x
print(f"use_global sees: {x}") # Output: use_global sees: global
def use_nonlocal():
nonlocal x # Refers to outer's x
print(f"use_nonlocal sees: {x}") # Output: use_nonlocal sees: enclosing
use_global()
use_nonlocal()
outer()globalalways refers to the module-level scopenonlocalrefers to the nearest enclosing function scope
When You Can't Use nonlocal
The nonlocal keyword only works with enclosing function scopes. You can't use it for:
- Global scope (use
globalinstead):
x = "global"
def func():
nonlocal x # SyntaxError: no binding for nonlocal 'x' found
x = "modified"- Variables that don't exist in any enclosing scope:
def outer():
def inner():
nonlocal count # SyntaxError: no binding for nonlocal 'count' foundKey Takeaways About nonlocal
- Use
nonlocalto modify variables from enclosing function scopes nonlocalsearches enclosing function scopes, not global scope- Reading enclosing variables doesn't require
nonlocal- only modifying them does nonlocalenables powerful patterns for creating functions with private state- We'll learn more about closures and factory functions in Chapter 23
The nonlocal keyword is particularly useful for creating functions that maintain private state, as we saw with the counter, account, and event tracker examples.
21.5) Deleting Names (Not Objects) with del and What It Means
Sometimes you need to remove a variable from your program's namespace - perhaps to free memory in long-running programs, clean up temporary variables, or remove entries from collections. Python's del statement handles these tasks, but it's important to understand exactly what it does and doesn't do.
The del statement in Python is often misunderstood. It doesn't delete objects - it deletes names (variable bindings). Understanding this distinction is crucial for understanding how Python manages memory and references.
What del Actually Does
The del statement removes a name from the current scope:
x = 42
print(x) # Output: 42
del x
# print(x) # NameError: name 'x' is not definedAfter del x, the name x no longer exists in the current scope. If you try to use it, Python raises a NameError because the name isn't defined anymore.
Deleting Names vs Deleting Objects
This is the key insight: del removes the name, not necessarily the object the name refers to:
# Create a list and two names that refer to it
original = [1, 2, 3]
reference = original # Both names refer to the same list
print(original) # Output: [1, 2, 3]
print(reference) # Output: [1, 2, 3]
# Delete one name
del original
# The list still exists because 'reference' still refers to it
print(reference) # Output: [1, 2, 3]
# print(original) # NameError: name 'original' is not definedThe list [1, 2, 3] continues to exist because reference still refers to it. Deleting original only removed that name - it didn't delete the list object itself.
When Objects Are Actually Deleted
Python automatically deletes objects when they're no longer referenced by any name. This is called garbage collection:
data = [1, 2, 3] # List is created, 'data' refers to it
del data # 'data' name is deleted
# Now the list has no references, so Python will eventually delete it
# (This happens automatically - you don't need to do anything)When we delete data, the list [1, 2, 3] has no remaining references, so Python's garbage collector will eventually reclaim the memory. But this happens automatically - you don't control when.
Deleting Items from Collections
The del statement can also remove items from collections, but this is fundamentally different from deleting names. When you use del with collection indexing or slicing, you're modifying the collection itself, not deleting a name.
This is an important distinction: when you write del numbers[2], you're calling a special method on the list object to remove an element. The name numbers still exists and still refers to the same list object - the list just has fewer elements now.
# Deleting list elements by index
numbers = [10, 20, 30, 40, 50]
del numbers[2] # Remove the element at index 2
print(numbers) # Output: [10, 20, 40, 50]
# Deleting list slices
numbers = [10, 20, 30, 40, 50]
del numbers[1:3] # Remove elements from index 1 to 3 (exclusive)
print(numbers) # Output: [10, 40, 50]
# Deleting dictionary entries
person = {'name': 'Alice', 'age': 30, 'city': 'Boston'}
del person['age']
print(person) # Output: {'name': 'Alice', 'city': 'Boston'}