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()
message ← Hello from global scope
19# Global variable - defined at module level #?globalvar20message→ Hello from global scope = "Hello from global scope"def main():
1#@var=default,shadow2def main():3 print("=== Local vs Global Variables ===\n")4 5 global_demo() #?globaloutput=== Local vs Global Variables ===def global_demo(): # Can READ global without keyword
5 global_demo() #?global6 7 print("\n=== Local Variables ===")8 local_demo()9 10 print("\n=== Checking Global After Function ===")11 print(f"message is still: '{message}'") #?persist12 13 #@var=_,!14 print("\n=== Shadowing ===")15 shadow_demo()16 print(f"Global message unchanged: '{message}'")17 #@var=_,!1819# Global variable - defined at module level #?globalvar20message = "Hello from global scope"2122def global_demo():23 # Can READ global without keyword24 print("Inside function, reading global:")25 print(f" message = '{messageHello from global scope}'")outputInside function, reading global: message = 'Hello from global scope' === Local Variables ===greeting ← Hello from local scope, count ← 42
7 print("\n=== Local Variables ===")8 local_demo()9 10 print("\n=== Checking Global After Function ===")11 print(f"message is still: '{messageHello from global scope}'") #?persist12 13 #@var=_,!14 print("\n=== Shadowing ===")15 shadow_demo()16 print(f"Global message unchanged: '{message}'")17 #@var=_,!1819# Global variable - defined at module level #?globalvar20message = "Hello from global scope"2122def global_demo():23 # Can READ global without keyword24 print("Inside function, reading global:")25 print(f" message = '{message}'")2627def local_demo(): #?localvar28 # This creates a LOCAL variable, doesn't affect global29 greeting→ Hello from local scope = "Hello from local scope"30 count→ 42 = 4231 print(f" greeting = '{greetingHello from local scope}'")32 print(f" count = {count42}")33 # greeting and count die when function returnsoutput greeting = 'Hello from local scope' count = 42 === Checking Global After Function === message is still: 'Hello from global scope' === Shadowing ===message ← I'm a local variable!
14 print("\n=== Shadowing ===")15 shadow_demo()16 print(f"Global message unchanged: '{messageHello from global scope}'")17 #@var=_,!1819# Global variable - defined at module level #?globalvar20message = "Hello from global scope"2122def global_demo():23 # Can READ global without keyword24 print("Inside function, reading global:")25 print(f" message = '{message}'")2627def local_demo(): #?localvar28 # This creates a LOCAL variable, doesn't affect global29 greeting = "Hello from local scope"30 count = 4231 print(f" greeting = '{greeting}'")32 print(f" count = {count}")33 # greeting and count die when function returns3435#@var=_,!36def shadow_demo():37 # This 'message' shadows the global one38 message→ I'm a local variable! = "I'm a local variable!" #?shadowvar39 print(f" Local message: '{messageI'm a local variable!}'")40 # Global 'message' is unchangedoutput Local message: 'I'm a local variable!' Global message unchanged: 'Hello from global scope'main()
43if __name__ == "__main__":44 main()45#@help global
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()
x ← global x
15x→ global x = "global x" # Global scope #?globalxdef main():
1def main():2 print("=== LEGB Rule ===\n")3 print("Python searches names in order:")4 print("L → Local (current function)")5 print("E → Enclosing (outer function)")6 print("G → Global (module level)")7 print("B → Built-in (Python's names)")8 9 print("\n" + "="*40)10 demonstrate_legb()output=== LEGB Rule === Python searches names in order: L → Local (current function) E → Enclosing (outer function) G → Global (module level) B → Built-in (Python's names) ========================================x ← enclosing x
17def demonstrate_legb():18 print("\n--- LEGB Demonstration ---\n")19 20 x→ enclosing x = "enclosing x" # Enclosing scope #?enclosing21 22 def inner():23 x = "local x" # Local scope #?local24 print(f"In inner(): x = '{x}'") # Finds local first25 26 def inner_no_local(): #?nolocalx27 # No local x here28 print(f"In inner_no_local(): x = '{x}'") # Finds enclosing29 30 inner()31 inner_no_local()output --- LEGB Demonstration ---x ← local x
22def inner():23 x→ local x = "local x" # Local scope #?local24 print(f"In inner(): x = '{xlocal x}'") # Finds local firstoutputIn inner(): x = 'local x'inner()
30inner()31inner_no_local()32print(f"In outer(): x = '{x}'") # Uses enclosingdef inner_no_local(): #?nolocalx # No local x here
26def inner_no_local(): #?nolocalx27 # No local x here28 print(f"In inner_no_local(): x = '{xenclosing x}'") # Finds enclosingoutputIn inner_no_local(): x = 'enclosing x'print(f"In outer(): x = '{x}'") # Uses enclosing
9 print("\n" + "="*40)10 demonstrate_legb()11 12 print("\n=== Built-in Scope ===") #?builtin13 demonstrate_builtin()1415x = "global x" # Global scope #?globalx1617def demonstrate_legb():18 print("\n--- LEGB Demonstration ---\n")19 20 x = "enclosing x" # Enclosing scope #?enclosing21 22 def inner():23 x = "local x" # Local scope #?local24 print(f"In inner(): x = '{x}'") # Finds local first25 26 def inner_no_local(): #?nolocalx27 # No local x here28 print(f"In inner_no_local(): x = '{x}'") # Finds enclosing29 30 inner()31 inner_no_local()32 print(f"In outer(): x = '{xenclosing x}'") # Uses enclosingoutputIn outer(): x = 'enclosing x' === Built-in Scope ===numbers ← [1, 2, 3, 4, 5]
12 print("\n=== Built-in Scope ===") #?builtin13 demonstrate_builtin()1415x = "global x" # Global scope #?globalx1617def demonstrate_legb():18 print("\n--- LEGB Demonstration ---\n")19 20 x = "enclosing x" # Enclosing scope #?enclosing21 22 def inner():23 x = "local x" # Local scope #?local24 print(f"In inner(): x = '{x}'") # Finds local first25 26 def inner_no_local(): #?nolocalx27 # No local x here28 print(f"In inner_no_local(): x = '{x}'") # Finds enclosing29 30 inner()31 inner_no_local()32 print(f"In outer(): x = '{x}'") # Uses enclosing3334def demonstrate_builtin():35 # len is a built-in function #?builtinuse36 numbers→ [1, 2, 3, 4, 5] = [1, 2, 3, 4, 5]37 print(f"len({numbers[1, 2, 3, 4, 5]}) = {len(numbers)}")outputlen([1, 2, 3, 4, 5]) = 5main()
42if __name__ == "__main__":43 main()44#@help globalx
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()
counter ← 0, total ← 0
25counter→ 0 = 0 # Global variable #?globalcounter2627def show_counter():28 # Reading global - no keyword needed29 print(f"Counter is: {counter}")3031def increment_counter(): #?globalkey32 global counter # Required to MODIFY global33 old = counter34 counter += 135 print(f"Incremented: {old} → {counter}")3637def compare_approaches():38 # Without global - creates local! #?withoutkw39 def broken_increment():40 # counter = counter + 1 # Error! Or creates local41 local_counter = 042 local_counter += 143 print(f"Local only: {local_counter}")44 45 broken_increment()46 print(f"Global unchanged: {counter}")4748#@var=_,!49total→ 0 = 0def main():
1#@var=default,badPractice2def main():3 print("=== The 'global' Keyword ===\n")4 5 print(f"Initial counter: {counter0}")6 7 # This works - reading global #?read8 show_counter()output=== The 'global' Keyword === Initial counter: 0def show_counter(): # Reading global - no keyword needed
7 # This works - reading global #?read8 show_counter()9 10 # This modifies global #?modify11 increment_counter()12 increment_counter()13 increment_counter()14 15 print(f"\nFinal counter: {counter}")16 17 print("\n=== Without vs With 'global' ===")18 compare_approaches()19 20 #@var=_,!21 print("\n=== Why Global is Often Bad ===")22 demonstrate_problem()23 #@var=_,!2425counter = 0 # Global variable #?globalcounter2627def show_counter():28 # Reading global - no keyword needed29 print(f"Counter is: {counter0}")outputCounter is: 0old ← 0, counter ← 1
pass 1 of 310 # This modifies global #?modify11 increment_counter()12 increment_counter()13 increment_counter()14 15 print(f"\nFinal counter: {counter}")16 17 print("\n=== Without vs With 'global' ===")18 compare_approaches()19 20 #@var=_,!21 print("\n=== Why Global is Often Bad ===")22 demonstrate_problem()23 #@var=_,!2425counter = 0 # Global variable #?globalcounter2627def show_counter():28 # Reading global - no keyword needed29 print(f"Counter is: {counter}")3031def increment_counter(): #?globalkey32 global counter # Required to MODIFY global33 old→ 0 = counter034 counter→ 1 += 135 print(f"Incremented: {old0} → {counter1}")outputIncremented: 0 → 1All 3 passes — pass 1 is the card above pass oldcounterlocal_counter1 0 0 → 1 — 2 1 1 → 2 — 3 2 2 → 3 1 → 0 local_counter ← 0
38# Without global - creates local! #?withoutkw39def broken_increment():40 # counter = counter + 1 # Error! Or creates local41 local_counter→ 0 = 042 local_counter→ 1 += 143 print(f"Local only: {local_counter1}")outputLocal only: 1print(f"Global unchanged: {counter}")
17 print("\n=== Without vs With 'global' ===")18 compare_approaches()19 20 #@var=_,!21 print("\n=== Why Global is Often Bad ===")22 demonstrate_problem()23 #@var=_,!2425counter = 0 # Global variable #?globalcounter2627def show_counter():28 # Reading global - no keyword needed29 print(f"Counter is: {counter}")3031def increment_counter(): #?globalkey32 global counter # Required to MODIFY global33 old = counter34 counter += 135 print(f"Incremented: {old} → {counter}")3637def compare_approaches():38 # Without global - creates local! #?withoutkw39 def broken_increment():40 # counter = counter + 1 # Error! Or creates local41 local_counter = 042 local_counter += 143 print(f"Local only: {local_counter}")44 45 broken_increment()46 print(f"Global unchanged: {counter3}")outputGlobal unchanged: 3 === Why Global is Often Bad ===total ← 0
55def demonstrate_problem():56 global total57 total→ 0 = 058 59 add_to_total(10)60 add_to_total(20)total ← 10
pass 1 of 251def add_to_total(value10): #?badpractice52 global total53 total→ 10 += value10add_to_total(10)
59add_to_total(10)60add_to_total(20)61print(f"Total: {total}")total ← 30
pass 2 of 251def add_to_total(value20): #?badpractice52 global total53 total→ 30 += value20print(f"Total: {total}")
21 print("\n=== Why Global is Often Bad ===")22 demonstrate_problem()23 #@var=_,!2425counter = 0 # Global variable #?globalcounter2627def show_counter():28 # Reading global - no keyword needed29 print(f"Counter is: {counter}")3031def increment_counter(): #?globalkey32 global counter # Required to MODIFY global33 old = counter34 counter += 135 print(f"Incremented: {old} → {counter}")3637def compare_approaches():38 # Without global - creates local! #?withoutkw39 def broken_increment():40 # counter = counter + 1 # Error! Or creates local41 local_counter = 042 local_counter += 143 print(f"Local only: {local_counter}")44 45 broken_increment()46 print(f"Global unchanged: {counter}")4748#@var=_,!49total = 05051def add_to_total(value): #?badpractice52 global total53 total += value5455def demonstrate_problem():56 global total57 total = 058 59 add_to_total(10)60 add_to_total(20)61 print(f"Total: {total30}")62 63 print("\nProblem: Hard to track who modifies 'total'")64 print("Better: Return values instead of modifying globals")65#@var=_,!outputTotal: 30 Problem: Hard to track who modifies 'total' Better: Return values instead of modifying globalsmain()
67if __name__ == "__main__":68 main()69#@help globalcounter
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()
def main():
1def main():2 print("=== The 'nonlocal' Keyword ===\n")3 4 counter_example()output=== The 'nonlocal' Keyword ===count ← 0
12def counter_example():13 print("--- Counter with Closure ---")14 15 count→ 0 = 0 # Enclosing scope variable #?enclosing16 17 def increment(): #?nonlocalkw18 nonlocal count # Modify enclosing, not create local19 count += 120 print(f"Count: {count}")21 22 def decrement():23 nonlocal count24 count -= 125 print(f"Count: {count}")26 27 increment()28 increment()output--- Counter with Closure ---count ← 1
pass 1 of 317def increment(): #?nonlocalkw18 nonlocal count # Modify enclosing, not create local19 count→ 1 += 120 print(f"Count: {count1}")outputCount: 1All 3 passes — pass 1 is the card above pass count1 0 → 1 2 1 → 2 3 2 → 3 increment()
27increment()28increment()29increment()increment()
27increment()28increment()29increment()30decrement()increment()
28increment()29increment()30decrement()count ← 2
22def decrement():23 nonlocal count24 count→ 2 -= 125 print(f"Count: {count2}")outputCount: 2print(f"Final count: {count}")
4 counter_example()5 6 print("\n=== Accumulator Example ===")7 accumulator_example()8 9 print("\n=== Without nonlocal ===")10 without_nonlocal()1112def counter_example():13 print("--- Counter with Closure ---")14 15 count = 0 # Enclosing scope variable #?enclosing16 17 def increment(): #?nonlocalkw18 nonlocal count # Modify enclosing, not create local19 count += 120 print(f"Count: {count}")21 22 def decrement():23 nonlocal count24 count -= 125 print(f"Count: {count}")26 27 increment()28 increment()29 increment()30 decrement()31 32 print(f"Final count: {count2}")outputFinal count: 2 === Accumulator Example ===total ← 0
34def accumulator_example(): #?accumulator35 print("--- Running Total ---")36 37 total→ 0 = 038 39 def add(value):40 nonlocal total41 total += value42 return total43 44 print(f"Add 10: {add(10)}")45 print(f"Add 5: {add(5)}")output--- Running Total ---total ← 10
pass 1 of 439def add(value10):40 nonlocal total41 total→ 10 += value1042 return total10All 4 passes — pass 1 is the card above pass valuetotal1 10 0 → 10 2 5 10 → 15 3 3 15 → 18 4 7 18 → 25 print(f"Add 10: {add(10)}")
44print(f"Add 10: {add(10)}")45print(f"Add 5: {add(5)}")46print(f"Add 3: {add(3)}")outputAdd 10: 10print(f"Add 5: {add(5)}")
44print(f"Add 10: {add(10)}")45print(f"Add 5: {add(5)}")46print(f"Add 3: {add(3)}")47print(f"Add 7: {add(7)}")outputAdd 5: 15print(f"Add 3: {add(3)}")
45print(f"Add 5: {add(5)}")46print(f"Add 3: {add(3)}")47print(f"Add 7: {add(7)}")outputAdd 3: 18print(f"Add 7: {add(7)}")
6 print("\n=== Accumulator Example ===")7 accumulator_example()8 9 print("\n=== Without nonlocal ===")10 without_nonlocal()1112def counter_example():13 print("--- Counter with Closure ---")14 15 count = 0 # Enclosing scope variable #?enclosing16 17 def increment(): #?nonlocalkw18 nonlocal count # Modify enclosing, not create local19 count += 120 print(f"Count: {count}")21 22 def decrement():23 nonlocal count24 count -= 125 print(f"Count: {count}")26 27 increment()28 increment()29 increment()30 decrement()31 32 print(f"Final count: {count}")3334def accumulator_example(): #?accumulator35 print("--- Running Total ---")36 37 total = 038 39 def add(value):40 nonlocal total41 total += value42 return total43 44 print(f"Add 10: {add(10)}")45 print(f"Add 5: {add(5)}")46 print(f"Add 3: {add(3)}")47 print(f"Add 7: {add(7)}")outputAdd 7: 25 === Without nonlocal ===message ← original
49def without_nonlocal(): #?without50 print("--- Without nonlocal (fails) ---")51 52 message→ original = "original"53 54 def try_modify():55 # Without nonlocal, this creates a LOCAL variable56 message = "modified" # New local, doesn't touch outer!57 print(f"Inside: {message}")58 59 try_modify()60 print(f"Outside: {message}") # Still "original"!output--- Without nonlocal (fails) ---message ← modified
54def try_modify():55 # Without nonlocal, this creates a LOCAL variable56 message→ modified = "modified" # New local, doesn't touch outer!57 print(f"Inside: {messagemodified}")outputInside: modifiedprint(f"Outside: {message}") # Still "original"!
9 print("\n=== Without nonlocal ===")10 without_nonlocal()1112def counter_example():13 print("--- Counter with Closure ---")14 15 count = 0 # Enclosing scope variable #?enclosing16 17 def increment(): #?nonlocalkw18 nonlocal count # Modify enclosing, not create local19 count += 120 print(f"Count: {count}")21 22 def decrement():23 nonlocal count24 count -= 125 print(f"Count: {count}")26 27 increment()28 increment()29 increment()30 decrement()31 32 print(f"Final count: {count}")3334def accumulator_example(): #?accumulator35 print("--- Running Total ---")36 37 total = 038 39 def add(value):40 nonlocal total41 total += value42 return total43 44 print(f"Add 10: {add(10)}")45 print(f"Add 5: {add(5)}")46 print(f"Add 3: {add(3)}")47 print(f"Add 7: {add(7)}")4849def without_nonlocal(): #?without50 print("--- Without nonlocal (fails) ---")51 52 message = "original"53 54 def try_modify():55 # Without nonlocal, this creates a LOCAL variable56 message = "modified" # New local, doesn't touch outer!57 print(f"Inside: {message}")58 59 try_modify()60 print(f"Outside: {messageoriginal}") # Still "original"!outputOutside: originalmain()
62if __name__ == "__main__":63 main()64#@help enclosing
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 = 2
triple_factor = 3
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 = 4
triple_factor = 3
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 = 2
triple_factor = 5
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():
1#@var=default,practical2def main():3 print("=== Closures ===\n")4 print("A closure 'remembers' variables from enclosing scope")5 print("even after the outer function has returned.\n")6 7 # Create closures #?create8 counter_a = make_counter()9 counter_b = make_counter()output=== Closures === A closure 'remembers' variables from enclosing scope even after the outer function has returned.count ← 0
pass 1 of 238def make_counter(): #?closure39 count→ 0 = 0 # Captured by inner function40 41 def counter():42 nonlocal count43 count += 144 return count45 46 return counter<function make_counter.<locals>.counter at ⟨addr A⟩> # Return the inner functioncounter_a ← <function make_counter.<locals>.counter at ⟨addr A⟩>
7# Create closures #?create8counter_a→ <function make_counter.<locals>.counter at ⟨addr A⟩> = make_counter()9counter_b = make_counter()count ← 0
pass 2 of 238def make_counter(): #?closure39 count→ 0 = 0 # Captured by inner function40 41 def counter():42 nonlocal count43 count += 144 return count45 46 return counter<function make_counter.<locals>.counter at ⟨addr B⟩> # Return the inner functioncounter_b ← <function make_counter.<locals>.counter at ⟨addr B⟩>
8counter_a = make_counter()9counter_b→ <function make_counter.<locals>.counter at ⟨addr B⟩> = make_counter()1011print("Two independent counters:")12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")outputTwo independent counters:count ← 1
pass 1 of 541def counter():42 nonlocal count43 count→ 1 += 144 return count1All 5 passes — pass 1 is the card above pass count1 0 → 1 2 1 → 2 3 2 → 3 4 0 → 1 5 1 → 2 print(f"counter_a(): {counter_a()}")
11print("Two independent counters:")12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")14print(f"counter_a(): {counter_a()}")outputcounter_a(): 1print(f"counter_a(): {counter_a()}")
12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")14print(f"counter_a(): {counter_a()}")15print(f"counter_b(): {counter_b()}") # Independent!outputcounter_a(): 2print(f"counter_a(): {counter_a()}")
13print(f"counter_a(): {counter_a()}")14print(f"counter_a(): {counter_a()}")15print(f"counter_b(): {counter_b()}") # Independent!16print(f"counter_b(): {counter_b()}")outputcounter_a(): 3print(f"counter_b(): {counter_b()}") # Independent!
14print(f"counter_a(): {counter_a()}")15print(f"counter_b(): {counter_b()}") # Independent!16print(f"counter_b(): {counter_b()}")outputcounter_b(): 1double_factor ← 2, triple_factor ← 3
15print(f"counter_b(): {counter_b()}") # Independent!16print(f"counter_b(): {counter_b()}")1718print("\n=== Multiplier Factory ===") #?factory19double_factor→ 2 = 2 #@double_factor=420triple_factor→ 3 = 3 #@triple_factor=521double = make_multiplier(double_factor2)22triple = make_multiplier(triple_factor)outputcounter_b(): 2 === Multiplier Factory ===def make_multiplier(factor): #?capture # 'factor' is captured by …
pass 1 of 248def make_multiplier(factor2): #?capture49 # 'factor' is captured by inner function50 def multiply(x):51 return x * factor # Uses captured factor52 53 return multiply<function make_multiplier.<locals>.multiply at ⟨addr C⟩>double ← <function make_multiplier.<locals>.multiply at ⟨addr C⟩>
20triple_factor = 3 #@triple_factor=521double→ <function make_multiplier.<locals>.multiply at ⟨addr C⟩> = make_multiplier(double_factor2)22triple = make_multiplier(triple_factor3)def make_multiplier(factor): #?capture # 'factor' is captured by …
pass 2 of 248def make_multiplier(factor3): #?capture49 # 'factor' is captured by inner function50 def multiply(x):51 return x * factor # Uses captured factor52 53 return multiply<function make_multiplier.<locals>.multiply at ⟨addr D⟩>triple ← <function make_multiplier.<locals>.multiply at ⟨addr D⟩>
21double = make_multiplier(double_factor)22triple→ <function make_multiplier.<locals>.multiply at ⟨addr D⟩> = make_multiplier(triple_factor3)2324print(f"times {double_factor2}(5) = {double(5)}")25print(f"times {triple_factor}(5) = {triple(5)}")def multiply(x):
pass 1 of 349# 'factor' is captured by inner function50def multiply(x5):51 return x5 * factor2 # Uses captured factorAll 3 passes — pass 1 is the card above pass xfactor1 5 2 2 5 3 3 10 2 print(f"times {double_factor}(5) = {double(5)}")
24print(f"times {double_factor2}(5) = {double(5)}")25print(f"times {triple_factor3}(5) = {triple(5)}")26print(f"times {double_factor}(10) = {double(10)}")outputtimes 2(5) = 10print(f"times {triple_factor}(5) = {triple(5)}")
24print(f"times {double_factor}(5) = {double(5)}")25print(f"times {triple_factor3}(5) = {triple(5)}")26print(f"times {double_factor2}(10) = {double(10)}")outputtimes 3(5) = 15print(f"times {double_factor}(10) = {double(10)}")
25print(f"times {triple_factor}(5) = {triple(5)}")26print(f"times {double_factor2}(10) = {double(10)}")2728#@var=_,!29print("\n=== Practical: Logger ===")30info_log = make_logger("INFO")31error_log = make_logger("ERROR")outputtimes 2(10) = 20 === Practical: Logger ===def make_logger(level): #?practical
pass 1 of 255#@var=_,!56def make_logger(levelINFO): #?practical57 """Factory for creating loggers with preset level."""58 def log(message):59 print(f"[{level}] {message}")60 return log<function make_logger.<locals>.log at ⟨addr E⟩>61#@var=_,!info_log ← <function make_logger.<locals>.log at ⟨addr E⟩>
29print("\n=== Practical: Logger ===")30info_log→ <function make_logger.<locals>.log at ⟨addr E⟩> = make_logger("INFO")31error_log = make_logger("ERROR")def make_logger(level): #?practical
pass 2 of 255#@var=_,!56def make_logger(levelERROR): #?practical57 """Factory for creating loggers with preset level."""58 def log(message):59 print(f"[{level}] {message}")60 return log<function make_logger.<locals>.log at ⟨addr F⟩>61#@var=_,!error_log ← <function make_logger.<locals>.log at ⟨addr F⟩>
30info_log = make_logger("INFO")31error_log→ <function make_logger.<locals>.log at ⟨addr F⟩> = make_logger("ERROR")3233info_log("Application started")34info_log("Loading config")def log(message):
pass 1 of 333 info_log("Application started")34 info_log("Loading config")35 error_log("Connection failed!")36 #@var=_,!3738def make_counter(): #?closure39 count = 0 # Captured by inner function40 41 def counter():42 nonlocal count43 count += 144 return count45 46 return counter # Return the inner function4748def make_multiplier(factor): #?capture49 # 'factor' is captured by inner function50 def multiply(x):51 return x * factor # Uses captured factor52 53 return multiply5455#@var=_,!56def make_logger(level): #?practical57 """Factory for creating loggers with preset level."""58 def log(messageApplication started):59 print(f"[{levelINFO}] {messageApplication started}")60 return logoutput[INFO] Application startedAll 3 passes — pass 1 is the card above pass messagelevel1 Application started INFO 2 Loading config INFO 3 Connection failed! ERROR main()
63if __name__ == "__main__":64 main()65#@help create
def main():
1def main():2 print("=== Closures ===\n")3 print("A closure 'remembers' variables from enclosing scope")4 print("even after the outer function has returned.\n")5 6 # Create closures7 counter_a = make_counter()8 counter_b = make_counter()output=== Closures === A closure 'remembers' variables from enclosing scope even after the outer function has returned.count ← 0
pass 1 of 235def make_counter():36 count→ 0 = 0 # Captured by inner function37 38 def counter():39 nonlocal count40 count += 141 return count42 43 return counter<function make_counter.<locals>.counter at ⟨addr A⟩> # Return the inner functioncounter_a ← <function make_counter.<locals>.counter at ⟨addr A⟩>
6# Create closures7counter_a→ <function make_counter.<locals>.counter at ⟨addr A⟩> = make_counter()8counter_b = make_counter()count ← 0
pass 2 of 235def make_counter():36 count→ 0 = 0 # Captured by inner function37 38 def counter():39 nonlocal count40 count += 141 return count42 43 return counter<function make_counter.<locals>.counter at ⟨addr B⟩> # Return the inner functioncounter_b ← <function make_counter.<locals>.counter at ⟨addr B⟩>
7counter_a = make_counter()8counter_b→ <function make_counter.<locals>.counter at ⟨addr B⟩> = make_counter()910print("Two independent counters:")11print(f"counter_a(): {counter_a()}")12print(f"counter_a(): {counter_a()}")outputTwo independent counters:count ← 1
pass 1 of 538def counter():39 nonlocal count40 count→ 1 += 141 return count1All 5 passes — pass 1 is the card above pass count1 0 → 1 2 1 → 2 3 2 → 3 4 0 → 1 5 1 → 2 print(f"counter_a(): {counter_a()}")
10print("Two independent counters:")11print(f"counter_a(): {counter_a()}")12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")outputcounter_a(): 1print(f"counter_a(): {counter_a()}")
11print(f"counter_a(): {counter_a()}")12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")14print(f"counter_b(): {counter_b()}") # Independent!outputcounter_a(): 2print(f"counter_a(): {counter_a()}")
12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")14print(f"counter_b(): {counter_b()}") # Independent!15print(f"counter_b(): {counter_b()}")outputcounter_a(): 3print(f"counter_b(): {counter_b()}") # Independent!
13print(f"counter_a(): {counter_a()}")14print(f"counter_b(): {counter_b()}") # Independent!15print(f"counter_b(): {counter_b()}")outputcounter_b(): 1double_factor ← 4, triple_factor ← 3
14print(f"counter_b(): {counter_b()}") # Independent!15print(f"counter_b(): {counter_b()}")1617print("\n=== Multiplier Factory ===")18double_factor→ 4 = 419triple_factor→ 3 = 320double = make_multiplier(double_factor4)21triple = make_multiplier(triple_factor)outputcounter_b(): 2 === Multiplier Factory ===def make_multiplier(factor): # 'factor' is captured by inner funct…
pass 1 of 245def make_multiplier(factor4):46 # 'factor' is captured by inner function47 def multiply(x):48 return x * factor # Uses captured factor49 50 return multiply<function make_multiplier.<locals>.multiply at ⟨addr C⟩>double ← <function make_multiplier.<locals>.multiply at ⟨addr C⟩>
19triple_factor = 320double→ <function make_multiplier.<locals>.multiply at ⟨addr C⟩> = make_multiplier(double_factor4)21triple = make_multiplier(triple_factor3)def make_multiplier(factor): # 'factor' is captured by inner funct…
pass 2 of 245def make_multiplier(factor3):46 # 'factor' is captured by inner function47 def multiply(x):48 return x * factor # Uses captured factor49 50 return multiply<function make_multiplier.<locals>.multiply at ⟨addr D⟩>triple ← <function make_multiplier.<locals>.multiply at ⟨addr D⟩>
20double = make_multiplier(double_factor)21triple→ <function make_multiplier.<locals>.multiply at ⟨addr D⟩> = make_multiplier(triple_factor3)2223print(f"times {double_factor4}(5) = {double(5)}")24print(f"times {triple_factor}(5) = {triple(5)}")def multiply(x):
pass 1 of 346# 'factor' is captured by inner function47def multiply(x5):48 return x5 * factor4 # Uses captured factorAll 3 passes — pass 1 is the card above pass xfactor1 5 4 2 5 3 3 10 4 print(f"times {double_factor}(5) = {double(5)}")
23print(f"times {double_factor4}(5) = {double(5)}")24print(f"times {triple_factor3}(5) = {triple(5)}")25print(f"times {double_factor}(10) = {double(10)}")outputtimes 4(5) = 20print(f"times {triple_factor}(5) = {triple(5)}")
23print(f"times {double_factor}(5) = {double(5)}")24print(f"times {triple_factor3}(5) = {triple(5)}")25print(f"times {double_factor4}(10) = {double(10)}")outputtimes 3(5) = 15print(f"times {double_factor}(10) = {double(10)}")
24print(f"times {triple_factor}(5) = {triple(5)}")25print(f"times {double_factor4}(10) = {double(10)}")2627print("\n=== Practical: Logger ===")28info_log = make_logger("INFO")29error_log = make_logger("ERROR")outputtimes 4(10) = 40 === Practical: Logger ===def make_logger(level):
pass 1 of 252def make_logger(levelINFO):53 """Factory for creating loggers with preset level."""54 def log(message):55 print(f"[{level}] {message}")56 return log<function make_logger.<locals>.log at ⟨addr E⟩>info_log ← <function make_logger.<locals>.log at ⟨addr E⟩>
27print("\n=== Practical: Logger ===")28info_log→ <function make_logger.<locals>.log at ⟨addr E⟩> = make_logger("INFO")29error_log = make_logger("ERROR")def make_logger(level):
pass 2 of 252def make_logger(levelERROR):53 """Factory for creating loggers with preset level."""54 def log(message):55 print(f"[{level}] {message}")56 return log<function make_logger.<locals>.log at ⟨addr F⟩>error_log ← <function make_logger.<locals>.log at ⟨addr F⟩>
28info_log = make_logger("INFO")29error_log→ <function make_logger.<locals>.log at ⟨addr F⟩> = make_logger("ERROR")3031info_log("Application started")32info_log("Loading config")def log(message):
pass 1 of 331 info_log("Application started")32 info_log("Loading config")33 error_log("Connection failed!")3435def make_counter():36 count = 0 # Captured by inner function37 38 def counter():39 nonlocal count40 count += 141 return count42 43 return counter # Return the inner function4445def make_multiplier(factor):46 # 'factor' is captured by inner function47 def multiply(x):48 return x * factor # Uses captured factor49 50 return multiply5152def make_logger(level):53 """Factory for creating loggers with preset level."""54 def log(messageApplication started):55 print(f"[{levelINFO}] {messageApplication started}")56 return logoutput[INFO] Application startedAll 3 passes — pass 1 is the card above pass messagelevel1 Application started INFO 2 Loading config INFO 3 Connection failed! ERROR main()
58if __name__ == "__main__":59 main()
def main():
1def main():2 print("=== Closures ===\n")3 print("A closure 'remembers' variables from enclosing scope")4 print("even after the outer function has returned.\n")5 6 # Create closures7 counter_a = make_counter()8 counter_b = make_counter()output=== Closures === A closure 'remembers' variables from enclosing scope even after the outer function has returned.count ← 0
pass 1 of 235def make_counter():36 count→ 0 = 0 # Captured by inner function37 38 def counter():39 nonlocal count40 count += 141 return count42 43 return counter<function make_counter.<locals>.counter at ⟨addr A⟩> # Return the inner functioncounter_a ← <function make_counter.<locals>.counter at ⟨addr A⟩>
6# Create closures7counter_a→ <function make_counter.<locals>.counter at ⟨addr A⟩> = make_counter()8counter_b = make_counter()count ← 0
pass 2 of 235def make_counter():36 count→ 0 = 0 # Captured by inner function37 38 def counter():39 nonlocal count40 count += 141 return count42 43 return counter<function make_counter.<locals>.counter at ⟨addr B⟩> # Return the inner functioncounter_b ← <function make_counter.<locals>.counter at ⟨addr B⟩>
7counter_a = make_counter()8counter_b→ <function make_counter.<locals>.counter at ⟨addr B⟩> = make_counter()910print("Two independent counters:")11print(f"counter_a(): {counter_a()}")12print(f"counter_a(): {counter_a()}")outputTwo independent counters:count ← 1
pass 1 of 538def counter():39 nonlocal count40 count→ 1 += 141 return count1All 5 passes — pass 1 is the card above pass count1 0 → 1 2 1 → 2 3 2 → 3 4 0 → 1 5 1 → 2 print(f"counter_a(): {counter_a()}")
10print("Two independent counters:")11print(f"counter_a(): {counter_a()}")12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")outputcounter_a(): 1print(f"counter_a(): {counter_a()}")
11print(f"counter_a(): {counter_a()}")12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")14print(f"counter_b(): {counter_b()}") # Independent!outputcounter_a(): 2print(f"counter_a(): {counter_a()}")
12print(f"counter_a(): {counter_a()}")13print(f"counter_a(): {counter_a()}")14print(f"counter_b(): {counter_b()}") # Independent!15print(f"counter_b(): {counter_b()}")outputcounter_a(): 3print(f"counter_b(): {counter_b()}") # Independent!
13print(f"counter_a(): {counter_a()}")14print(f"counter_b(): {counter_b()}") # Independent!15print(f"counter_b(): {counter_b()}")outputcounter_b(): 1double_factor ← 2, triple_factor ← 5
14print(f"counter_b(): {counter_b()}") # Independent!15print(f"counter_b(): {counter_b()}")1617print("\n=== Multiplier Factory ===")18double_factor→ 2 = 219triple_factor→ 5 = 520double = make_multiplier(double_factor2)21triple = make_multiplier(triple_factor)outputcounter_b(): 2 === Multiplier Factory ===def make_multiplier(factor): # 'factor' is captured by inner funct…
pass 1 of 245def make_multiplier(factor2):46 # 'factor' is captured by inner function47 def multiply(x):48 return x * factor # Uses captured factor49 50 return multiply<function make_multiplier.<locals>.multiply at ⟨addr C⟩>double ← <function make_multiplier.<locals>.multiply at ⟨addr C⟩>
19triple_factor = 520double→ <function make_multiplier.<locals>.multiply at ⟨addr C⟩> = make_multiplier(double_factor2)21triple = make_multiplier(triple_factor5)def make_multiplier(factor): # 'factor' is captured by inner funct…
pass 2 of 245def make_multiplier(factor5):46 # 'factor' is captured by inner function47 def multiply(x):48 return x * factor # Uses captured factor49 50 return multiply<function make_multiplier.<locals>.multiply at ⟨addr D⟩>triple ← <function make_multiplier.<locals>.multiply at ⟨addr D⟩>
20double = make_multiplier(double_factor)21triple→ <function make_multiplier.<locals>.multiply at ⟨addr D⟩> = make_multiplier(triple_factor5)2223print(f"times {double_factor2}(5) = {double(5)}")24print(f"times {triple_factor}(5) = {triple(5)}")def multiply(x):
pass 1 of 346# 'factor' is captured by inner function47def multiply(x5):48 return x5 * factor2 # Uses captured factorAll 3 passes — pass 1 is the card above pass xfactor1 5 2 2 5 5 3 10 2 print(f"times {double_factor}(5) = {double(5)}")
23print(f"times {double_factor2}(5) = {double(5)}")24print(f"times {triple_factor5}(5) = {triple(5)}")25print(f"times {double_factor}(10) = {double(10)}")outputtimes 2(5) = 10print(f"times {triple_factor}(5) = {triple(5)}")
23print(f"times {double_factor}(5) = {double(5)}")24print(f"times {triple_factor5}(5) = {triple(5)}")25print(f"times {double_factor2}(10) = {double(10)}")outputtimes 5(5) = 25print(f"times {double_factor}(10) = {double(10)}")
24print(f"times {triple_factor}(5) = {triple(5)}")25print(f"times {double_factor2}(10) = {double(10)}")2627print("\n=== Practical: Logger ===")28info_log = make_logger("INFO")29error_log = make_logger("ERROR")outputtimes 2(10) = 20 === Practical: Logger ===def make_logger(level):
pass 1 of 252def make_logger(levelINFO):53 """Factory for creating loggers with preset level."""54 def log(message):55 print(f"[{level}] {message}")56 return log<function make_logger.<locals>.log at ⟨addr E⟩>info_log ← <function make_logger.<locals>.log at ⟨addr E⟩>
27print("\n=== Practical: Logger ===")28info_log→ <function make_logger.<locals>.log at ⟨addr E⟩> = make_logger("INFO")29error_log = make_logger("ERROR")def make_logger(level):
pass 2 of 252def make_logger(levelERROR):53 """Factory for creating loggers with preset level."""54 def log(message):55 print(f"[{level}] {message}")56 return log<function make_logger.<locals>.log at ⟨addr F⟩>error_log ← <function make_logger.<locals>.log at ⟨addr F⟩>
28info_log = make_logger("INFO")29error_log→ <function make_logger.<locals>.log at ⟨addr F⟩> = make_logger("ERROR")3031info_log("Application started")32info_log("Loading config")def log(message):
pass 1 of 331 info_log("Application started")32 info_log("Loading config")33 error_log("Connection failed!")3435def make_counter():36 count = 0 # Captured by inner function37 38 def counter():39 nonlocal count40 count += 141 return count42 43 return counter # Return the inner function4445def make_multiplier(factor):46 # 'factor' is captured by inner function47 def multiply(x):48 return x * factor # Uses captured factor49 50 return multiply5152def make_logger(level):53 """Factory for creating loggers with preset level."""54 def log(messageApplication started):55 print(f"[{levelINFO}] {messageApplication started}")56 return logoutput[INFO] Application startedAll 3 passes — pass 1 is the card above pass messagelevel1 Application started INFO 2 Loading config INFO 3 Connection failed! ERROR main()
58if __name__ == "__main__":59 main()
A closure "remembers" variables from where it was defined, not where it's called.
See the Closure Boxes
A closure is a function plus the values it captured. These diagrams pin the exact counters and multipliers from closure.py.
Exercise: scope_gotchas.py
Explore common scope-related mistakes and how to avoid them