Python Scope
Master variable visibility and lifetime in Python programs
🔍 What is Scope?
Scope determines where variables can be accessed in your code. Understanding scope is crucial for writing maintainable programs, avoiding naming conflicts, and managing memory efficiently.
# Global scope example
global_var = "I'm global" # Accessible everywhere
def my_function():
local_var = "I'm local" # Only accessible inside function
print(global_var) # Can access global
print(local_var) # Can access local
# Can access global_var here
print(global_var)
# Can't access local_var here
# print(local_var) # This would cause an error
Understanding Scope Theory
Python follows the LEGB rule for variable resolution, which defines the order in which Python searches for variables:
🎯 The LEGB Rule
L - Local
Variables defined inside the current function
E - Enclosing
Variables in the local scope of enclosing functions
G - Global
Variables defined at the module level
B - Built-in
Pre-defined names in Python (print, len, etc.)
🧠 Key Principles
- Variable Lifetime: Variables exist only within their scope
- Name Resolution: Python searches scopes in LEGB order
- Assignment Creates Local: Assigning to a variable makes it local by default
- Reading vs Writing: Different rules apply for reading and modifying variables
Local Scope
Local scope refers to variables defined inside a function. They're only accessible within that function:
# Basic local scope
def greet():
message = "Hello, World!" # Local variable
print(message)
greet() # Output: Hello, World!
# This would cause an error - message is not accessible outside the function
# print(message) # NameError: name 'message' is not defined
# Function parameters are also local variables
def add_numbers(a, b): # a and b are local to this function
result = a + b # result is also local
return result
sum_result = add_numbers(5, 3)
print(sum_result) # 8
# a, b, and result are not accessible here
# print(a) # NameError
# Local variables in different functions are independent
def function_one():
x = 10
print(f"In function_one: x = {x}")
def function_two():
x = 20 # Different x, local to function_two
print(f"In function_two: x = {x}")
function_one() # In function_one: x = 10
function_two() # In function_two: x = 20
💡 Local Scope Tips
- Isolation: Local variables don't interfere with variables in other functions
- Memory: Local variables are automatically cleaned up when the function ends
- Parameters: Function parameters are treated as local variables
- Temporary Storage: Use local variables for temporary calculations
Global Scope
Global variables are defined at the module level and can be accessed from anywhere in the module:
# Global variables
app_name = "My Python App"
version = "1.0.0"
debug_mode = True
def show_app_info():
# Reading global variables (no special keyword needed)
print(f"App: {app_name}")
print(f"Version: {version}")
print(f"Debug: {debug_mode}")
show_app_info()
# Modifying global variables requires the 'global' keyword
counter = 0 # Global variable
def increment_counter():
global counter # Declare that we want to modify the global counter
counter += 1
print(f"Counter: {counter}")
def reset_counter():
global counter
counter = 0
print("Counter reset")
increment_counter() # Counter: 1
increment_counter() # Counter: 2
reset_counter() # Counter reset
increment_counter() # Counter: 1
# Without 'global', assignment creates a local variable
def confusing_function():
counter = 100 # This creates a NEW local variable
print(f"Local counter: {counter}")
confusing_function() # Local counter: 100
print(f"Global counter: {counter}") # Global counter: 1 (unchanged)
⚠️ Global Variable Warnings
- Avoid Overuse: Too many global variables make code hard to maintain
- Use Constants: Global constants (UPPER_CASE) are generally acceptable
- Thread Safety: Global variables can cause issues in multi-threaded programs
- Testing: Global state makes unit testing more difficult
Enclosing Scope (Closures)
Enclosing scope occurs when you have nested functions. Inner functions can access variables from outer functions:
# Basic enclosing scope
def outer_function():
outer_var = "I'm in the outer function"
def inner_function():
# Can access outer_var from enclosing scope
print(f"Inner function says: {outer_var}")
inner_function()
outer_function() # Inner function says: I'm in the outer function
# Closure example - inner function "remembers" outer variables
def create_multiplier(factor):
def multiply(number):
return number * factor # factor is from enclosing scope
return multiply
# Create specialized functions
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10 (5 * 2)
print(triple(4)) # 12 (4 * 3)
# Modifying enclosing variables with 'nonlocal'
def create_counter():
count = 0 # Variable in enclosing scope
def increment():
nonlocal count # Declare that we want to modify the enclosing variable
count += 1
return count
def decrement():
nonlocal count
count -= 1
return count
def get_count():
return count # Reading doesn't require nonlocal
return increment, decrement, get_count
# Create counter functions
inc, dec, get = create_counter()
print(inc()) # 1
print(inc()) # 2
print(dec()) # 1
print(get()) # 1
🎯 Closure Benefits
- Data Encapsulation: Keep related data and functions together
- Factory Functions: Create specialized functions dynamically
- State Preservation: Maintain state between function calls
- Callback Functions: Useful for event handling and decorators
Built-in Scope
Built-in scope contains pre-defined names that are always available in Python:
# Built-in functions are always available
print("Hello") # print is a built-in function
numbers = [1, 2, 3, 4, 5]
print(len(numbers)) # len is built-in
print(max(numbers)) # max is built-in
print(sum(numbers)) # sum is built-in
# View all built-in names
import builtins
print(dir(builtins)) # Shows all built-in names
# You can accidentally shadow built-in names (don't do this!)
def demonstrate_shadowing():
# This creates a local variable that shadows the built-in 'len'
len = "I'm not the len function anymore!"
print(len) # Prints the string, not the function
# This would cause an error now:
# print(len([1, 2, 3])) # TypeError: 'str' object is not callable
demonstrate_shadowing()
# After the function, built-in 'len' is still available
print(len([1, 2, 3])) # Works fine - prints 3
# Common built-in names you use regularly
data = [1, 2, 3, 4, 5]
print(f"Length: {len(data)}")
print(f"Type: {type(data)}")
print(f"Range: {list(range(5))}")
print(f"Sorted: {sorted(data, reverse=True)}")
# Built-in exceptions
try:
result = 10 / 0
except ZeroDivisionError: # ZeroDivisionError is built-in
print("Cannot divide by zero!")
# Built-in constants
print(f"True: {True}") # True is built-in
print(f"False: {False}") # False is built-in
print(f"None: {None}") # None is built-in
📚 Built-in Categories
Functions
print(), len(), max(), min(), sum(), sorted(), etc.
Types
int, str, list, dict, tuple, set, etc.
Exceptions
ValueError, TypeError, IndexError, etc.
Constants
True, False, None, NotImplemented, etc.
Scope Resolution in Action
Let's see how Python resolves variable names using the LEGB rule:
# LEGB Rule demonstration
x = "Global x" # Global scope
def outer():
x = "Enclosing x" # Enclosing scope
def inner():
x = "Local x" # Local scope
print(f"Inner function sees: {x}") # Local x
def inner_no_local():
print(f"Inner (no local) sees: {x}") # Enclosing x
inner()
inner_no_local()
print(f"Outer function sees: {x}") # Enclosing x
outer()
print(f"Global scope sees: {x}") # Global x
# Example with built-in shadowing
def scope_example():
# This shadows the built-in 'max' function
max = "I'm not the max function!"
def inner():
# This creates a local 'max'
max = 42
print(f"Local max: {max}")
inner()
print(f"Enclosing max: {max}")
scope_example()
# Built-in max is still available here
print(f"Built-in max works: {max([1, 5, 3])}")
# Practical example: Configuration system
DEFAULT_CONFIG = { # Global
'debug': False,
'timeout': 30,
'retries': 3
}
def create_api_client(base_url):
config = DEFAULT_CONFIG.copy() # Enclosing scope
def make_request(endpoint, custom_timeout=None):
# Local scope
timeout = custom_timeout or config['timeout'] # Uses enclosing config
debug = config['debug'] # Uses enclosing config
if debug:
print(f"Making request to {base_url}/{endpoint} with timeout {timeout}")
return f"Response from {base_url}/{endpoint}"
def set_debug(enabled):
nonlocal config
config = config.copy() # Don't modify the original
config['debug'] = enabled
return make_request, set_debug
# Usage
api_request, set_debug = create_api_client("https://api.example.com")
result = api_request("users")
print(result)
set_debug(True)
result = api_request("posts", custom_timeout=60)
print(result)
Scope Best Practices
- Minimize Global Variables: Use them sparingly, prefer function parameters
- Use Constants: Global constants (UPPER_CASE) are acceptable
- Clear Naming: Use descriptive names to avoid confusion
- Limit Scope: Keep variables in the smallest scope possible
- Document Closures: Explain complex closure behavior
- Shadowing Built-ins: Don't use names like 'len', 'max', 'sum'
- Global Overuse: Too many globals make code hard to test
- Unclear Modifications: Always use 'global' or 'nonlocal' when modifying
- Deep Nesting: Avoid too many nested function levels
- Mutable Defaults: Be careful with mutable default arguments
# Good: Use constants for configuration
API_BASE_URL = "https://api.example.com" # Global constant
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
# Good: Pass data as parameters instead of using globals
def process_data(data, config):
"""Process data with given configuration."""
timeout = config.get('timeout', DEFAULT_TIMEOUT)
# Process with local variables
return f"Processed {len(data)} items with timeout {timeout}"
# Good: Use closures for factory functions
def create_validator(min_length, max_length):
"""Create a validator function with specific constraints."""
def validate(text):
if len(text) < min_length:
return f"Text too short (minimum {min_length})"
if len(text) > max_length:
return f"Text too long (maximum {max_length})"
return "Valid"
return validate
# Usage
email_validator = create_validator(5, 50)
password_validator = create_validator(8, 128)
print(email_validator("[email protected]")) # Valid
print(password_validator("123")) # Text too short (minimum 8)
# Good: Clear variable names and limited scope
def calculate_total_price(items, tax_rate=0.08):
"""Calculate total price including tax."""
subtotal = sum(item['price'] for item in items)
tax_amount = subtotal * tax_rate
total = subtotal + tax_amount
return {
'subtotal': subtotal,
'tax': tax_amount,
'total': total
}
# Good: Use classes to manage state instead of global variables
class Counter:
def __init__(self, initial_value=0):
self._value = initial_value
def increment(self):
self._value += 1
return self._value
def get_value(self):
return self._value
# Usage
counter = Counter()
print(counter.increment()) # 1
print(counter.increment()) # 2
🧠 Test Your Knowledge
Test your understanding of Python scope: