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
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
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
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
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
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
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)
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
Global variables should be rare, reserved for true program-wide state:
LIBRARY_NAME = "Alexandria" # Module constant
MAX_CHECKOUTS = 5 # Configuration value
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)