DEV Community

Cover image for The Scope Investigation: Local, Global, and Nonlocal Variables
Aaron Rose
Aaron Rose

Posted on

The Scope Investigation: Local, Global, and Nonlocal Variables

Timothy had mastered Python's data structures, but Margaret sensed he was ready for a fundamental shift. She led him away from the archive chambers to a new wing of the library—The Experimental Workshop—where functions lived and operated. "Data structures hold information," she explained, "but functions transform it. And every function operates in its own private laboratory with strict rules about what it can see and modify."

The Private Laboratory

Margaret demonstrated the most basic principle with a simple experiment:

library_name = "Alexandria"  # Global variable

def catalog_book(book_title: str) -> str:
    shelf_assignment = "A-12"  # Local variable
    return f"{book_title} assigned to {shelf_assignment}"

result = catalog_book("1984")
print(result)  # "1984 assigned to A-12"
print(shelf_assignment)  # NameError: name 'shelf_assignment' is not defined
Enter fullscreen mode Exit fullscreen mode

The shelf_assignment variable existed only inside the function's private laboratory. Once the function finished, that variable vanished like steam from a beaker. Timothy tried to access it from outside and received Python's stern rejection.

"Each function gets its own namespace," Margaret explained. "Variables created inside belong exclusively to that function. They're born when the function runs and destroyed when it returns."

The Global View

Timothy noticed the function could read library_name even though it wasn't created inside. Margaret showed him the scope hierarchy:

total_books = 0  # Global scope

def add_to_collection(book_count: int) -> None:
    # Can READ global variables
    print(f"Current collection: {total_books}")
    print(f"Adding: {book_count}")

add_to_collection(5)  # Works fine for reading
Enter fullscreen mode Exit fullscreen mode

Functions could peek outside their laboratory to read global variables. But modification required explicit permission:

total_books = 100

def add_books(new_count: int) -> None:
    total_books = total_books + new_count  # UnboundLocalError!

add_books(5)  # Fails
Enter fullscreen mode Exit fullscreen mode

Python saw the assignment to total_books and decided Timothy must want a local variable. But then it encountered that same variable being used on the right side of the assignment—before it had been created. Python rejected this: "You're trying to use a variable you told me was local before I've had a chance to create it."

The Global Declaration

Margaret revealed the global keyword, which granted functions permission to modify variables in the outer library—like a special pass allowing Timothy to step outside his laboratory and update the main library catalog:

total_books = 100

def add_books(new_count: int) -> None:
    global total_books  # Permission to modify library catalog
    total_books = total_books + new_count

add_books(5)
print(total_books)  # 105
Enter fullscreen mode Exit fullscreen mode

The global declaration told Python: "I'm not creating a new local variable; I'm modifying the one in the main library." Timothy could now reach beyond his laboratory's walls to adjust the library's master catalog.

The Nested Laboratory Problem

Margaret introduced Timothy to nested functions—laboratories within laboratories:

def create_catalog_system(library_name: str):
    book_count = 0  # Enclosing scope

    def add_book(title: str) -> None:
        book_count = book_count + 1  # UnboundLocalError!
        print(f"Added {title} to {library_name}")

    add_book("1984")
    return book_count

create_catalog_system("Alexandria")  # Fails
Enter fullscreen mode Exit fullscreen mode

The inner function could read library_name from the enclosing scope, but modifying book_count caused the same paradox as before. The global keyword wouldn't help—book_count wasn't global; it belonged to the outer function.

The Nonlocal Solution

Margaret introduced nonlocal, designed specifically for nested functions:

def create_catalog_system(library_name: str):
    book_count = 0  # Enclosing scope

    def add_book(title: str) -> None:
        nonlocal book_count  # Permission for enclosing scope
        book_count = book_count + 1
        print(f"Added {title} to {library_name}")
        print(f"Total books: {book_count}")

    add_book("1984")
    add_book("Dune")
    return book_count

total = create_catalog_system("Alexandria")
print(total)  # 2
Enter fullscreen mode Exit fullscreen mode

The nonlocal keyword told Python: "Find this variable in the nearest enclosing scope and let me modify it." Unlike global, which always looked at module level, nonlocal worked with the immediate outer function.

The Scope Search Order

Timothy learned Python's scope resolution follows LEGB—like a systematic search through increasingly distant library sections:

Local → Enclosing → Global → Built-in

Think of it as Python searching from Timothy's immediate workbench, to Margaret's surrounding laboratory, to the main library catalog, and finally to the universal reference books that every library shares. Python uses the first match it finds.

len = 100  # Global (shadows built-in!)

def outer_function():
    len = 50  # Enclosing scope

    def inner_function():
        len = 10  # Local scope
        print(len)  # Prints 10 (finds Local first)

    inner_function()
    print(len)  # Prints 50 (finds Enclosing)

outer_function()
print(len)  # Prints 100 (finds Global)
Enter fullscreen mode Exit fullscreen mode

Python searched from the inside out, using the first match it found. This explained why shadowing built-ins was dangerous—you could accidentally override Python's fundamental tools by defining a variable with the same name at a closer scope level.

The Practical Wisdom

Margaret showed Timothy when each scope type mattered:

Local variables were the default and best practice—they kept functions self-contained:

def calculate_late_fee(days_overdue: int) -> float:
    daily_rate = 0.50  # Local, temporary calculation
    return days_overdue * daily_rate
Enter fullscreen mode Exit fullscreen mode

Global variables should be rare, reserved for true program-wide state:

LIBRARY_NAME = "Alexandria"  # Module constant
MAX_CHECKOUTS = 5  # Configuration value
Enter fullscreen mode Exit fullscreen mode

Nonlocal variables enabled sophisticated patterns with nested functions, particularly for maintaining state across multiple inner function calls.

Timothy's Function Wisdom

Through exploring The Experimental Workshop, Timothy learned essential principles:

Functions create private namespaces: Local variables exist only during function execution and cannot be accessed from outside.

Reading vs. writing differs: Functions can read enclosing scopes freely, but modification requires global or nonlocal declarations.

LEGB defines search order: Python looks Local, then Enclosing, then Global, then Built-in when resolving names.

Global should be rare: Excessive global variable use creates tangled dependencies; prefer parameters and return values.

Nonlocal enables nested state: Inner functions can modify outer function variables with explicit nonlocal permission.

Scope shadows hide outer names: Defining a local variable with the same name as an outer one makes the outer unreachable within that scope.

Timothy's investigation of scope revealed that functions were more than isolated experiments—they were sophisticated systems with carefully controlled access to the library's resources. Understanding scope meant understanding exactly what each function could see, modify, and protect. The Experimental Workshop operated on principles of encapsulation and controlled access, making Python's functions both powerful and predictable.

But Margaret had one more mystery to reveal. "Now that you understand how functions access their enclosing scopes," she said with a knowing smile, "you're ready to discover something remarkable: functions that capture those scopes and carry them forward in time. This principle of remembering and manipulating outer scope variables is the foundation for an even more advanced concept—closures—the subject of our next investigation."


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)