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 global and nonlocal keywords 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