Functions & Scope
Variable Scope and Lifetime
You assign x = 10 inside a function. Outside, x is undefined. Python follows
the LEGB rule to find variables: Local, Enclosing, Global, Built-in. Understanding
scope prevents confusing bugs.
Local vs global variables
Understand where variables are visible.
def main():
print("=== Local vs Global Variables ===\n")
global_demo()
print("\n=== Local Variables ===")
local_demo()
print("\n=== Checking Global After Function ===")
print(f"message is still: '{message}'")
print("\n=== Shadowing ===")
shadow_demo()
print(f"Global message unchanged: '{message}'")
# Global variable - defined at module level
message = "Hello from global scope"
def global_demo():
# Can READ global without keyword
print("Inside function, reading global:")
print(f" message = '{message}'")
def local_demo():
# This creates a LOCAL variable, doesn't affect global
greeting = "Hello from local scope"
count = 42
print(f" greeting = '{greeting}'")
print(f" count = {count}")
# greeting and count die when function returns
def shadow_demo():
# This 'message' shadows the global one
message = "I'm a local variable!"
print(f" Local message: '{message}'")
# Global 'message' is unchanged
if __name__ == "__main__":
main()
Variables assigned inside a function are local by default.
The LEGB rule
Python's variable lookup order.
def main():
print("=== LEGB Rule ===\n")
print("Python searches names in order:")
print("L → Local (current function)")
print("E → Enclosing (outer function)")
print("G → Global (module level)")
print("B → Built-in (Python's names)")
print("\n" + "="*40)
demonstrate_legb()
print("\n=== Built-in Scope ===")
demonstrate_builtin()
x = "global x" # Global scope
def demonstrate_legb():
print("\n--- LEGB Demonstration ---\n")
x = "enclosing x" # Enclosing scope
def inner():
x = "local x" # Local scope
print(f"In inner(): x = '{x}'") # Finds local first
def inner_no_local():
# No local x here
print(f"In inner_no_local(): x = '{x}'") # Finds enclosing
inner()
inner_no_local()
print(f"In outer(): x = '{x}'") # Uses enclosing
def demonstrate_builtin():
# len is a built-in function
numbers = [1, 2, 3, 4, 5]
print(f"len({numbers}) = {len(numbers)}")
# We could shadow it (don't do this!)
# len = 10 # This would break len()!
if __name__ == "__main__":
main()
Local → Enclosing → Global → Built-in. First match wins.
The global keyword
Modify a global variable from inside a function.
def main():
print("=== The 'global' Keyword ===\n")
print(f"Initial counter: {counter}")
# This works - reading global
show_counter()
# This modifies global
increment_counter()
increment_counter()
increment_counter()
print(f"\nFinal counter: {counter}")
print("\n=== Without vs With 'global' ===")
compare_approaches()
print("\n=== Why Global is Often Bad ===")
demonstrate_problem()
counter = 0 # Global variable
def show_counter():
# Reading global - no keyword needed
print(f"Counter is: {counter}")
def increment_counter():
global counter # Required to MODIFY global
old = counter
counter += 1
print(f"Incremented: {old} → {counter}")
def compare_approaches():
# Without global - creates local!
def broken_increment():
# counter = counter + 1 # Error! Or creates local
local_counter = 0
local_counter += 1
print(f"Local only: {local_counter}")
broken_increment()
print(f"Global unchanged: {counter}")
total = 0
def add_to_total(value):
global total
total += value
def demonstrate_problem():
global total
total = 0
add_to_total(10)
add_to_total(20)
print(f"Total: {total}")
print("\nProblem: Hard to track who modifies 'total'")
print("Better: Return values instead of modifying globals")
if __name__ == "__main__":
main()
Without global, assignment creates a local variable instead.
The nonlocal keyword
Modify an enclosing function's variable.
def main():
print("=== The 'nonlocal' Keyword ===\n")
counter_example()
print("\n=== Accumulator Example ===")
accumulator_example()
print("\n=== Without nonlocal ===")
without_nonlocal()
def counter_example():
print("--- Counter with Closure ---")
count = 0 # Enclosing scope variable
def increment():
nonlocal count # Modify enclosing, not create local
count += 1
print(f"Count: {count}")
def decrement():
nonlocal count
count -= 1
print(f"Count: {count}")
increment()
increment()
increment()
decrement()
print(f"Final count: {count}")
def accumulator_example():
print("--- Running Total ---")
total = 0
def add(value):
nonlocal total
total += value
return total
print(f"Add 10: {add(10)}")
print(f"Add 5: {add(5)}")
print(f"Add 3: {add(3)}")
print(f"Add 7: {add(7)}")
def without_nonlocal():
print("--- Without nonlocal (fails) ---")
message = "original"
def try_modify():
# Without nonlocal, this creates a LOCAL variable
message = "modified" # New local, doesn't touch outer!
print(f"Inside: {message}")
try_modify()
print(f"Outside: {message}") # Still "original"!
if __name__ == "__main__":
main()
nonlocal reaches into the enclosing function's scope.
Closures remember scope
Functions can capture variables from their enclosing scope.
def main():
print("=== Closures ===\n")
print("A closure 'remembers' variables from enclosing scope")
print("even after the outer function has returned.\n")
# Create closures
counter_a = make_counter()
counter_b = make_counter()
print("Two independent counters:")
print(f"counter_a(): {counter_a()}")
print(f"counter_a(): {counter_a()}")
print(f"counter_a(): {counter_a()}")
print(f"counter_b(): {counter_b()}") # Independent!
print(f"counter_b(): {counter_b()}")
print("\n=== Multiplier Factory ===")
double_factor =
triple_factor =
double = make_multiplier(double_factor)
triple = make_multiplier(triple_factor)
print(f"times {double_factor}(5) = {double(5)}")
print(f"times {triple_factor}(5) = {triple(5)}")
print(f"times {double_factor}(10) = {double(10)}")
print("\n=== Practical: Logger ===")
info_log = make_logger("INFO")
error_log = make_logger("ERROR")
info_log("Application started")
info_log("Loading config")
error_log("Connection failed!")
def make_counter():
count = 0 # Captured by inner function
def counter():
nonlocal count
count += 1
return count
return counter # Return the inner function
def make_multiplier(factor):
# 'factor' is captured by inner function
def multiply(x):
return x * factor # Uses captured factor
return multiply
def make_logger(level):
"""Factory for creating loggers with preset level."""
def log(message):
print(f"[{level}] {message}")
return log
if __name__ == "__main__":
main()
def main():
print("=== Closures ===\n")
print("A closure 'remembers' variables from enclosing scope")
print("even after the outer function has returned.\n")
# Create closures
counter_a = make_counter()
counter_b = make_counter()
print("Two independent counters:")
print(f"counter_a(): {counter_a()}")
print(f"counter_a(): {counter_a()}")
print(f"counter_a(): {counter_a()}")
print(f"counter_b(): {counter_b()}") # Independent!
print(f"counter_b(): {counter_b()}")
print("\n=== Multiplier Factory ===")
double_factor =
triple_factor =
double = make_multiplier(double_factor)
triple = make_multiplier(triple_factor)
print(f"times {double_factor}(5) = {double(5)}")
print(f"times {triple_factor}(5) = {triple(5)}")
print(f"times {double_factor}(10) = {double(10)}")
print("\n=== Practical: Logger ===")
info_log = make_logger("INFO")
error_log = make_logger("ERROR")
info_log("Application started")
info_log("Loading config")
error_log("Connection failed!")
def make_counter():
count = 0 # Captured by inner function
def counter():
nonlocal count
count += 1
return count
return counter # Return the inner function
def make_multiplier(factor):
# 'factor' is captured by inner function
def multiply(x):
return x * factor # Uses captured factor
return multiply
def make_logger(level):
"""Factory for creating loggers with preset level."""
def log(message):
print(f"[{level}] {message}")
return log
if __name__ == "__main__":
main()
def main():
print("=== Closures ===\n")
print("A closure 'remembers' variables from enclosing scope")
print("even after the outer function has returned.\n")
# Create closures
counter_a = make_counter()
counter_b = make_counter()
print("Two independent counters:")
print(f"counter_a(): {counter_a()}")
print(f"counter_a(): {counter_a()}")
print(f"counter_a(): {counter_a()}")
print(f"counter_b(): {counter_b()}") # Independent!
print(f"counter_b(): {counter_b()}")
print("\n=== Multiplier Factory ===")
double_factor =
triple_factor =
double = make_multiplier(double_factor)
triple = make_multiplier(triple_factor)
print(f"times {double_factor}(5) = {double(5)}")
print(f"times {triple_factor}(5) = {triple(5)}")
print(f"times {double_factor}(10) = {double(10)}")
print("\n=== Practical: Logger ===")
info_log = make_logger("INFO")
error_log = make_logger("ERROR")
info_log("Application started")
info_log("Loading config")
error_log("Connection failed!")
def make_counter():
count = 0 # Captured by inner function
def counter():
nonlocal count
count += 1
return count
return counter # Return the inner function
def make_multiplier(factor):
# 'factor' is captured by inner function
def multiply(x):
return x * factor # Uses captured factor
return multiply
def make_logger(level):
"""Factory for creating loggers with preset level."""
def log(message):
print(f"[{level}] {message}")
return log
if __name__ == "__main__":
main()
def main():
print("=== Closures ===\n")
print("A closure 'remembers' variables from enclosing scope")
print("even after the outer function has returned.\n")
# Create closures
counter_a = make_counter()
counter_b = make_counter()
print("Two independent counters:")
print(f"counter_a(): {counter_a()}")
print(f"counter_a(): {counter_a()}")
print(f"counter_a(): {counter_a()}")
print(f"counter_b(): {counter_b()}") # Independent!
print(f"counter_b(): {counter_b()}")
print("\n=== Multiplier Factory ===")
double_factor =
triple_factor =
double = make_multiplier(double_factor)
triple = make_multiplier(triple_factor)
print(f"times {double_factor}(5) = {double(5)}")
print(f"times {triple_factor}(5) = {triple(5)}")
print(f"times {double_factor}(10) = {double(10)}")
print("\n=== Practical: Logger ===")
info_log = make_logger("INFO")
error_log = make_logger("ERROR")
info_log("Application started")
info_log("Loading config")
error_log("Connection failed!")
def make_counter():
count = 0 # Captured by inner function
def counter():
nonlocal count
count += 1
return count
return counter # Return the inner function
def make_multiplier(factor):
# 'factor' is captured by inner function
def multiply(x):
return x * factor # Uses captured factor
return multiply
def make_logger(level):
"""Factory for creating loggers with preset level."""
def log(message):
print(f"[{level}] {message}")
return log
if __name__ == "__main__":
main()
A closure "remembers" variables from where it was defined, not where it's called.
Exercise: scope_gotchas.py
Explore common scope-related mistakes and how to avoid them