The LEGB Rule — How Python Resolves Variable Names
Imagine Python compiling a function as building a blueprint for a house. If the blueprint includes a plan for a 'local storage' room for api_endpoint, Python assumes api_endpoint will be local, regardless of whether that room ever gets built (the assignment executes). Any attempt to access api_endpoint before its local storage is 'built' will fail.
The Setup
You are designing a configuration manager. You want to track an API endpoint globally, but conditionally override or inspect it within nested utility functions.
What Does This Print?
api_endpoint = "https://api.production.internal"
def configure_client():
def check_status():
# Attempt to log global endpoint, then modify it locally if needed
print("Checking endpoint:", api_endpoint)
if False:
api_endpoint = "https://api.sandbox.internal"
check_status()
configure_client()
The Output
The program raises an UnboundLocalError. Even though the assignment api_endpoint = ... is inside an if False block that never executes, Python treats api_endpoint as a local variable throughout the entire scope of the check_status function. Thus, the print statement tries to read a local variable that has not been bound yet.
Why Python Does This
During the compilation phase (before execution), Python analyzes the AST (Abstract Syntax Tree) to determine variable scopes. Any assignment within a block (e.g., api_endpoint = ...) flags that variable name as local to the active code block's namespace. The compiler generates bytecode using the LOAD_FAST instruction for that variable. Unlike dynamic lookup languages, Python does not fallback to the enclosing scope at runtime if a local variable is unbound; it immediately raises UnboundLocalError because LOAD_FAST expects the value in the local stack array.
The Fix
api_endpoint = "https://api.production.internal"
def configure_client():
# Pass the endpoint explicitly so check_status has no reason
# to reach into an outer scope.
def check_status(endpoint):
print("Checking endpoint:", endpoint)
check_status(api_endpoint)
configure_client()
Passing api_endpoint as an explicit parameter gives check_status its own local binding from the moment it is called. There is no assignment inside the function body, so Python never treats api_endpoint as local, and the UnboundLocalError cannot occur. Parameter passing is preferred over global because it makes the dependency explicit and avoids coupling the inner function to module-level state.
How This Fails in Real Systems
A payments system utilized a local caching strategy with fallback to global configurations. An unreachable debug-mode assignment branch inside an inner verification loop caused a cascade of UnboundLocalError crashes on production checkout, blocking checkouts for 3 hours until the deployment was rolled back.