Python Scope
Learn about variable scope in Python and how Python resolves variable names using the LEGB rule.
Python Scope
A variable is only available from inside the region it is created. This is called scope.
Local Scope
A variable created inside a function belongs to the local scope of that function, and can only be used inside that function.
Example - A variable created inside a function is available inside that function:
def myfunc():
x = 300
print(x)
myfunc() Function Inside Function
As explained in the example above, the variable x is not available outside the function, but it is available for any function inside the function:
Example - The local variable can be accessed from a function within the function:
def myfunc():
x = 300
def myinnerfunc():
print(x)
myinnerfunc()
myfunc() Global Scope
A variable created in the main body of the Python code is a global variable and belongs to the global scope.
Global variables are available from within any scope, global and local.
Example - A variable created outside of a function is global and can be used by anyone:
x = 300
def myfunc():
print(x)
myfunc()
print(x) Naming Variables
If you operate with the same variable name inside and outside of a function, Python will treat them as two separate variables, one available in the global scope (outside the function) and one available in the local scope (inside the function):
Example - The function will print the local x, and then the code will print the global x:
x = 300
def myfunc():
x = 200
print(x)
myfunc()
print(x) Global Keyword
If you need to create a global variable, but are stuck in the local scope, you can use the global keyword.
The global keyword makes the variable global.
Example - If you use the global keyword, the variable belongs to the global scope:
def myfunc():
global x
x = 300
myfunc()
print(x) Also, use the global keyword if you want to make a change to a global variable inside a function.
Example - To change the value of a global variable inside a function, refer to the variable by using the global keyword:
x = 300
def myfunc():
global x
x = 200
myfunc()
print(x) Nonlocal Keyword
The nonlocal keyword is used to work with variables inside nested functions.
The nonlocal keyword makes the variable belong to the outer function.
Example - If you use the nonlocal keyword, the variable will belong to the outer function:
def myfunc1():
x = "Jane"
def myfunc2():
nonlocal x
x = "hello"
myfunc2()
return x
print(myfunc1()) LEGB Rule
Python follows the LEGB rule to resolve variable names:
- L - Local (inside the current function)
- E - Enclosing (in any outer function)
- G - Global (at the module level)
- B - Built-in (in the built-in namespace)
Example - LEGB rule demonstration:
# Built-in scope (B)
# print, len, str, etc. are built-in functions
# Global scope (G)
x = "global x"
def outer_function():
# Enclosing scope (E)
x = "enclosing x"
def inner_function():
# Local scope (L)
x = "local x"
print(f"Inner function: {x}") # Local x
def inner_function2():
print(f"Inner function 2: {x}") # Enclosing x
inner_function()
inner_function2()
print(f"Outer function: {x}") # Enclosing x
outer_function()
print(f"Global: {x}") # Global x Scope Examples
Example 1: Variable Shadowing
name = "Global Alice"
def greet():
name = "Local Bob" # This shadows the global variable
print(f"Hello, {name}!")
def greet_global():
print(f"Hello, {name}!") # Uses global variable
greet() # Hello, Local Bob!
greet_global() # Hello, Global Alice!
print(name) # Global Alice Example 2: Modifying Global Variables
counter = 0
def increment():
global counter
counter += 1
print(f"Counter: {counter}")
def increment_local():
counter = 10 # Creates a new local variable
counter += 1
print(f"Local counter: {counter}")
print(f"Initial counter: {counter}") # 0
increment() # Counter: 1
increment() # Counter: 2
increment_local() # Local counter: 11
print(f"Final counter: {counter}") # 2 Example 3: Nested Functions and Nonlocal
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
def decrement():
nonlocal count
count -= 1
return count
def get_count():
return count
return increment, decrement, get_count
# Create counter functions
inc, dec, get = make_counter()
print(get()) # 0
print(inc()) # 1
print(inc()) # 2
print(dec()) # 1
print(get()) # 1 Class Scope
Classes have their own scope rules. Class variables are shared among all instances:
Example - Class and instance variables:
class MyClass:
class_var = "I'm a class variable"
def __init__(self, name):
self.instance_var = name # Instance variable
def show_vars(self):
print(f"Class variable: {MyClass.class_var}")
print(f"Instance variable: {self.instance_var}")
def modify_class_var(self):
MyClass.class_var = "Modified class variable"
@classmethod
def class_method(cls):
print(f"Accessing class variable from class method: {cls.class_var}")
@staticmethod
def static_method():
print("Static method - no access to class or instance variables")
# Usage
obj1 = MyClass("Object 1")
obj2 = MyClass("Object 2")
obj1.show_vars()
obj2.show_vars()
print("\nAfter modifying class variable:")
obj1.modify_class_var()
obj1.show_vars()
obj2.show_vars() # Both objects see the change
MyClass.class_method()
MyClass.static_method() Scope and Loops
In Python, loop variables have function scope, not block scope:
Example - Loop variable scope:
def demonstrate_loop_scope():
# Loop variables persist after the loop
for i in range(3):
x = i * 2
print(f"i after loop: {i}") # i is still accessible
print(f"x after loop: {x}") # x is still accessible
# List comprehension has its own scope
squares = [y**2 for y in range(5)]
# print(y) # This would cause an error - y is not accessible
return squares
result = demonstrate_loop_scope()
print(f"Squares: {result}") Practical Examples
Example 1: Configuration Manager
class ConfigManager:
_instance = None
_config = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def set_config(self, key, value):
ConfigManager._config[key] = value
def get_config(self, key, default=None):
return ConfigManager._config.get(key, default)
def show_all_config(self):
for key, value in ConfigManager._config.items():
print(f"{key}: {value}")
# Global configuration
config = ConfigManager()
def setup_database():
# Function scope - modifying global config
config.set_config("db_host", "localhost")
config.set_config("db_port", 5432)
def set_credentials():
# Nested function scope - still modifying global config
config.set_config("db_user", "admin")
config.set_config("db_password", "secret")
set_credentials()
def setup_api():
config.set_config("api_key", "abc123")
config.set_config("api_url", "https://api.example.com")
# Setup configuration
setup_database()
setup_api()
print("Final configuration:")
config.show_all_config() Example 2: Closure Example
def create_multiplier(factor):
"""Creates a function that multiplies by a given factor"""
def multiplier(number):
# This function "closes over" the factor variable
return number * factor
return multiplier
def create_accumulator(initial_value=0):
"""Creates a function that accumulates values"""
total = initial_value
def accumulate(value):
nonlocal total
total += value
return total
def get_total():
return total
def reset():
nonlocal total
total = initial_value
# Return multiple functions that share the same scope
accumulate.get_total = get_total
accumulate.reset = reset
return accumulate
# Usage
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Accumulator example
acc = create_accumulator(10)
print(acc(5)) # 15
print(acc(3)) # 18
print(acc.get_total()) # 18
acc.reset()
print(acc.get_total()) # 10 Common Scope Pitfalls
Pitfall 1: Late Binding Closures
# Problem: All functions refer to the same variable
functions = []
for i in range(3):
functions.append(lambda: i) # All lambdas refer to the same 'i'
# All functions return 2 (the final value of i)
for func in functions:
print(func()) # 2, 2, 2
# Solution 1: Use default parameter
functions_fixed1 = []
for i in range(3):
functions_fixed1.append(lambda x=i: x)
for func in functions_fixed1:
print(func()) # 0, 1, 2
# Solution 2: Use closure
def make_func(n):
return lambda: n
functions_fixed2 = []
for i in range(3):
functions_fixed2.append(make_func(i))
for func in functions_fixed2:
print(func()) # 0, 1, 2 Pitfall 2: Mutable Default Arguments
# Problem: Mutable default argument
def add_item(item, target_list=[]): # Don't do this!
target_list.append(item)
return target_list
# The same list is reused across calls
list1 = add_item("first")
list2 = add_item("second")
print(list1) # ['first', 'second'] - unexpected!
print(list2) # ['first', 'second'] - same list!
# Solution: Use None as default
def add_item_fixed(item, target_list=None):
if target_list is None:
target_list = []
target_list.append(item)
return target_list
list3 = add_item_fixed("first")
list4 = add_item_fixed("second")
print(list3) # ['first']
print(list4) # ['second'] Best Practices
- Use descriptive variable names to avoid confusion
- Minimize use of global variables
- Use
globalandnonlocalkeywords explicitly when needed - Be careful with mutable default arguments
- Understand closure behavior with loops
- Keep functions small and focused to reduce scope complexity
- Use class methods and static methods appropriately