1
Scope and Variable Visibility
▶Understanding how Python finds and resolves variable names is fundamental to writing correct, predictable functions. Python follows a strict four-level hierarchy called the LEGB rule, and two special keywords — global and nonlocal — let functions reach into outer scopes when necessary.
- The LEGB Scope Rule
- Local scope contains names assigned within the current function. This is the first place Python looks when a name is referenced inside a function body.
def greet(): message = "hello" # local to greet() print(message) # found in local scope greet() # hello - Enclosing scope covers variables in any outer function when functions are nested. Python walks outward through each enclosing function in order, from innermost to outermost.
def outer(): city = "Paris" # enclosing variable def inner(): print(city) # found in enclosing scope inner() # Paris - Global scope holds names defined at the top level of the module. These names are visible throughout the entire file unless shadowed by a more local definition.
language = "Python" # global variable def show_language(): print(language) # found in global scope show_language() # Python - Built-in scope contains Python's pre-defined names such as
len,range, andprint. This is the last place Python checks and is always available without any import.numbers = [3, 1, 4, 1, 5] print(len(numbers)) # len found in built-in scope print(max(numbers)) # max found in built-in scope - Name resolution stops at the first scope where the name is found. If a name is discovered in the local scope, Python never inspects the enclosing, global, or built-in scopes for that same lookup.
len = "oops" # shadows built-in len in global scope def check(): len = 42 # shadows both global and built-in print(len) # 42 — stopped at local scope check() del len # restore built-in access - Local Variables
- Assignment inside a function creates a new local variable rather than modifying a global one. Even if a global variable with the same name exists, the assignment targets the local scope exclusively.
colour = "red" # global def paint(): colour = "blue" # new local variable, not the global print(colour) # blue paint() print(colour) # red — global unchanged - Local variables are created when the function is called and destroyed when it returns. They have no existence outside of a single function invocation, keeping memory usage efficient and state isolated.
def calculate(): result = 100 * 2 # exists only during this call return result print(calculate()) # 200 # print(result) # NameError — result does not exist here - Reading a local variable before assigning it raises an
UnboundLocalError. This happens because Python determines at compile time that any name assigned anywhere in a function is local to that function, even if the assignment appears later in the code.total = 10 def broken(): print(total) # UnboundLocalError! total = total + 1 # assignment makes total local throughout # broken() # uncommenting raises UnboundLocalError - Local scope provides encapsulation, preventing accidental modification of outer variables. Functions that rely only on local variables and their parameters are easier to test and reason about in isolation.
def area(width, height): surface = width * height # fully local, no side effects return surface print(area(5, 3)) # 15 - The
globalKeyword - Declaring
global variable_nameinside a function binds that name to the module-level variable. Any subsequent assignment or mutation of that name then affects the global object directly.score = 0 def add_points(n): global score score += n # modifies the global score add_points(10) add_points(5) print(score) # 15 - Without
global, assigning to a name inside a function always creates a local variable. This is a common source of confusion when a developer intends to update a global counter or flag but inadvertently creates a separate local binding instead.counter = 0 def try_increment(): counter = counter + 1 # UnboundLocalError — counter treated as local # fix: add global counter above this line - The
globalkeyword should be used sparingly to avoid hidden side effects. Functions that silently alter global state are harder to debug, test, and reuse because their behaviour depends on external context.# Prefer returning values over mutating globals def increment(value): return value + 1 # no global state touched counter = 0 counter = increment(counter) print(counter) # 1 - Accessing a global variable for reading does not require the
globalkeyword. Python will naturally find the name in the global scope through normal LEGB lookup as long as no local assignment of the same name exists in the function.app_name = "MyApp" def show_banner(): print(f"Welcome to {app_name}") # reads global — no keyword needed show_banner() # Welcome to MyApp - The
nonlocalKeyword nonlocalbinds a name to the nearest enclosing scope that contains that variable. Python searches outward through nested function layers until it finds the first enclosing function where the name is defined.def outer(): level = "outer" def inner(): nonlocal level # binds to outer()'s level level = "changed" print(level) # changed inner() print(level) # changed outer()- It enables nested functions to update, not just read, variables from an outer function. Without
nonlocal, an assignment in the inner function would create a new local variable, leaving the outer function's variable unchanged.def outer(): value = 10 def without_nonlocal(): value = 99 # new local — outer's value untouched print(value) # 99 without_nonlocal() print(value) # 10 — unchanged nonlocaldoes not reach all the way to the global scope; useglobalfor that. The two keywords are distinct tools for distinct situations —nonlocalis strictly for enclosing function scopes.x = "global" def outer(): x = "enclosing" def inner(): nonlocal x # reaches enclosing, NOT global x = "inner" inner() print(x) # inner outer() print(x) # global — untouched- This keyword is essential for implementing closures that maintain mutable state. A classic example is a counter factory where the inner function increments a count variable defined in the outer function, preserving the value across multiple calls to the inner function.
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment counter = make_counter() print(counter()) # 1 print(counter()) # 2 print(counter()) # 3
# LEGB and keyword demonstration
x = "global"
def outer():
x = "enclosing"
def inner():
nonlocal x # binds to outer()'s x
x = "modified by inner"
print(x) # modified by inner
inner()
print(x) # modified by inner
outer()
print(x) # global (unchanged)
# global keyword example
count = 0
def increment():
global count
count += 1
increment()
increment()
print(count) # 2
NotesExplains how Python resolves variable names using the LEGB rule and how the global and nonlocal keywords allow functions to modify variables in enclosing scopes. Slides: 1, 2, 3, 4.