Python Global, Local, and Nonlocal Variables

Variable scope determines where in your code a variable can be accessed and modified. Understanding scope is fundamental to writing Python code that behaves predictably and avoids subtle bugs. When...

Key Insights

  • Python resolves variable names using the LEGB rule (Local, Enclosing, Global, Built-in), which determines which scope takes precedence when multiple variables share the same name.
  • The global keyword allows functions to modify module-level variables, while nonlocal enables nested functions to modify variables from their enclosing scope—but both should be used sparingly.
  • Local variables are the safest choice for most programming tasks; excessive use of global variables creates hidden dependencies that make code harder to test, debug, and maintain.

Introduction to Variable Scope

Variable scope determines where in your code a variable can be accessed and modified. Understanding scope is fundamental to writing Python code that behaves predictably and avoids subtle bugs. When you reference a variable name, Python needs to know which variable you mean—the one defined in the current function, one from an outer function, or perhaps a module-level variable.

Python uses three primary scope types: local (variables within functions), global (module-level variables), and nonlocal (variables in enclosing function scopes). Each serves a specific purpose, and choosing the right scope prevents common errors like unintended variable modification or namespace pollution.

x = "global"

def outer():
    x = "enclosing"
    
    def inner():
        x = "local"
        print(f"Inner x: {x}")
    
    inner()
    print(f"Outer x: {x}")

outer()
print(f"Global x: {x}")

# Output:
# Inner x: local
# Outer x: enclosing
# Global x: global

This example shows three different variables named x, each existing in its own scope without interfering with the others.

Local Variables

Local variables are defined inside functions and exist only during function execution. They’re created when the function is called and destroyed when it returns. This isolation is a feature, not a limitation—it prevents functions from accidentally interfering with each other’s data.

def calculate_total(price, quantity):
    tax_rate = 0.08  # Local variable
    subtotal = price * quantity  # Local variable
    total = subtotal * (1 + tax_rate)  # Local variable
    return total

result = calculate_total(100, 3)
print(result)  # 324.0

# This will raise NameError: name 'tax_rate' is not defined
# print(tax_rate)

Local scope provides encapsulation. Each function invocation gets its own set of local variables, even if the same function is called recursively:

def countdown(n):
    count = n  # Each call gets its own 'count'
    print(count)
    if count > 0:
        countdown(count - 1)

countdown(3)
# Output: 3, 2, 1, 0

Two functions can use identical variable names without conflict because their local scopes are isolated:

def calculate_area(radius):
    pi = 3.14159
    result = pi * radius ** 2
    return result

def calculate_circumference(radius):
    pi = 3.14159  # Different variable, same name
    result = 2 * pi * radius
    return result

# Both 'pi' and 'result' variables coexist peacefully
print(calculate_area(5))
print(calculate_circumference(5))

Global Variables

Global variables are defined at the module level and can be accessed from anywhere in the module. You can read global variables inside functions without any special syntax, but modifying them requires the global keyword.

app_name = "MyApp"
version = "1.0"

def print_info():
    # Reading global variables works without 'global' keyword
    print(f"{app_name} v{version}")

print_info()  # MyApp v1.0

To modify a global variable from within a function, you must declare it with the global keyword:

counter = 0

def increment():
    global counter  # Required to modify global variable
    counter += 1

increment()
increment()
print(counter)  # 2

A common mistake is forgetting the global keyword when trying to modify a global variable. Without it, Python creates a new local variable instead:

total = 0

def add_to_total(amount):
    # This creates a LOCAL variable named 'total', doesn't modify global
    total = total + amount  # UnboundLocalError!

# add_to_total(10)  # Error: local variable 'total' referenced before assignment

The error occurs because Python sees the assignment total = ... and decides total is local. When it tries to read total on the right side of the equation, the local variable doesn’t exist yet.

Global variables should be used sparingly. They’re appropriate for true application-level configuration or state, but overusing them creates hidden dependencies that make code difficult to test and maintain. Constants (by convention, UPPERCASE names) are acceptable as globals:

MAX_CONNECTIONS = 100
API_ENDPOINT = "https://api.example.com"

The Nonlocal Keyword

The nonlocal keyword addresses a specific scenario: nested functions that need to modify variables from their enclosing (but not global) scope. This is particularly useful for closures and decorators.

def outer():
    count = 0
    
    def inner():
        # This would create a local variable, not modify outer's count
        # count = count + 1  # UnboundLocalError
        
        # nonlocal allows modifying the enclosing scope's variable
        nonlocal count
        count += 1
        return count
    
    return inner

counter = outer()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3

Without nonlocal, attempting to modify count would create a local variable in inner, leading to an error. The nonlocal keyword tells Python to look in enclosing function scopes for the variable.

Here’s a practical example—a function that creates customized validators:

def create_validator(min_value, max_value):
    validation_count = 0
    
    def validate(value):
        nonlocal validation_count
        validation_count += 1
        
        if value < min_value or value > max_value:
            return False, f"Validation #{validation_count}: Out of range"
        return True, f"Validation #{validation_count}: Valid"
    
    return validate

age_validator = create_validator(0, 120)
print(age_validator(25))   # (True, 'Validation #1: Valid')
print(age_validator(150))  # (False, 'Validation #2: Out of range')
print(age_validator(30))   # (True, 'Validation #3: Valid')

The validation_count persists across calls to validate because it’s captured in the closure, and nonlocal allows the inner function to modify it.

LEGB Rule (Lookup Order)

When Python encounters a variable name, it searches for it in a specific order: Local, Enclosing, Global, Built-in (LEGB). It uses the first match it finds.

x = "global x"

def outer():
    x = "enclosing x"
    
    def inner():
        x = "local x"
        print(x)  # Finds local x first
    
    inner()
    print(x)  # Finds enclosing x (no local x here)

outer()
print(x)  # Finds global x

# Output:
# local x
# enclosing x
# global x

Here’s a more complex example demonstrating the full LEGB resolution:

# Built-in scope: len() function exists here
x = "global"  # Global scope

def outer():
    x = "enclosing"  # Enclosing scope
    
    def inner():
        x = "local"  # Local scope
        print(f"Local: {x}")
        print(f"Built-in len available: {len([1, 2, 3])}")
    
    def inner2():
        # No local x, so Python finds enclosing x
        print(f"Enclosing: {x}")
    
    inner()
    inner2()

outer()

Understanding LEGB helps debug confusing situations where variables don’t have the values you expect.

Best Practices and Common Pitfalls

Minimize Global Variable Usage: Prefer passing data through function parameters and return values. This makes dependencies explicit and functions easier to test.

# Bad: Hidden dependency on global variable
total = 0

def add_sale(amount):
    global total
    total += amount

# Good: Explicit parameters and return values
def add_sale(current_total, amount):
    return current_total + amount

total = 0
total = add_sale(total, 100)
total = add_sale(total, 50)

Avoid Variable Shadowing: Don’t reuse names from outer scopes unless you have a good reason. It confuses readers and can cause bugs.

# Problematic: shadows built-in
def process_data(list):  # 'list' shadows built-in type
    # Now you can't use list() constructor
    result = []
    for item in list:
        result.append(item * 2)
    return result

# Better: use descriptive names
def process_data(data_list):
    result = []
    for item in data_list:
        result.append(item * 2)
    return result

Use Globals for Constants Only: If a value truly doesn’t change, a global constant is fine. For mutable state, consider classes or explicit state management.

# Acceptable: configuration constants
DATABASE_URL = "postgresql://localhost/mydb"
MAX_RETRIES = 3

# Better than globals: encapsulate state in a class
class ShoppingCart:
    def __init__(self):
        self.items = []
        self.total = 0
    
    def add_item(self, price):
        self.items.append(price)
        self.total += price

When to Use Each Scope:

  • Local: Default choice for all function-internal data
  • Global: Constants and truly application-wide configuration
  • Nonlocal: Closures and decorators that maintain state across calls

Understanding Python’s scoping rules prevents bugs and makes your code more maintainable. Default to local variables, use globals sparingly for constants, and reach for nonlocal only when implementing closures or decorators. When in doubt, explicit is better than implicit—pass data through function parameters rather than relying on outer scopes.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.