Python Basics — Scope & Closures Practice Exercises
Scope & Closures — Practice Exercises
Test your understanding of local/global scope, the LEGB rule, global/nonlocal keywords, and closures.
Exercise 1 — Predict the Output
What does this code print? Try to figure it out before running it:
x = 10
def outer():
x = 20
def inner():
x = 30
print("inner:", x)
inner()
print("outer:", x)
outer()
print("global:", x)
Show Solution
inner: 30
outer: 20
global: 10
Each x is a separate local variable in its own scope. None of the assignments affect the others because there’s no global or nonlocal keyword.
Exercise 2 — Counter Factory
Create a function make_counter(start=0) that returns a function. Each time the returned function is called, it returns the next number in the sequence.
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
counter_from_10 = make_counter(10)
print(counter_from_10()) # 11
print(counter_from_10()) # 12
Show Solution
def make_counter(start=0):
count = start
def counter():
nonlocal count
count += 1
return count
return counter
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
counter_from_10 = make_counter(10)
print(counter_from_10()) # 11
print(counter_from_10()) # 12
The nonlocal keyword lets the inner function modify count from the enclosing scope.
Exercise 3 — Rate Limiter
Write a closure make_rate_limiter(max_calls) that returns a function. The returned function accepts any single argument, prints it, but stops printing after max_calls invocations (and prints “Rate limit exceeded” instead).
limited_print = make_rate_limiter(3)
limited_print("Hello") # Hello
limited_print("World") # World
limited_print("Python") # Python
limited_print("Extra") # Rate limit exceeded
limited_print("More") # Rate limit exceeded
Show Solution
def make_rate_limiter(max_calls):
calls = 0
def limited(message):
nonlocal calls
if calls < max_calls:
calls += 1
print(message)
else:
print("Rate limit exceeded")
return limited
limited_print = make_rate_limiter(3)
limited_print("Hello") # Hello
limited_print("World") # World
limited_print("Python") # Python
limited_print("Extra") # Rate limit exceeded
limited_print("More") # Rate limit exceeded
Exercise 4 — Fix the Bug
This code has scope-related bugs. Fix them so the output matches the expected result:
# Bug 1
total = 0
def add_to_total(n):
total = total + n
return total
# Expected: add_to_total(5) returns 5, add_to_total(3) returns 8
# Bug 2
def make_functions():
funcs = []
for i in range(5):
funcs.append(lambda: i * 10)
return funcs
# Expected: [f() for f in make_functions()] should be [0, 10, 20, 30, 40]
Show Solution
# Fix 1: Use global (or better, redesign to avoid global)
total = 0
def add_to_total(n):
global total
total = total + n
return total
print(add_to_total(5)) # 5
print(add_to_total(3)) # 8
# Fix 2: Capture current value with default argument
def make_functions():
funcs = []
for i in range(5):
funcs.append(lambda i=i: i * 10) # i=i captures current value
return funcs
print([f() for f in make_functions()]) # [0, 10, 20, 30, 40]
Bug 1: Assigning to total makes Python treat it as local. Fix with global keyword.
Bug 2: Lambda closures capture the variable i, not its value. By the time they run, i is 4. Fix by using a default argument i=i.
Exercise 5 — Configurable Validator
Create a closure make_validator(min_val, max_val) that returns a function. The returned function checks whether a number is within the range and returns a tuple (is_valid, message).
age_validator = make_validator(0, 150)
print(age_validator(25)) # (True, '25 is valid (0-150)')
print(age_validator(-5)) # (False, '-5 is below minimum 0')
print(age_validator(200)) # (False, '200 is above maximum 150')
score_validator = make_validator(0, 100)
print(score_validator(85)) # (True, '85 is valid (0-100)')
Show Solution
def make_validator(min_val, max_val):
def validate(value):
if value < min_val:
return (False, f"{value} is below minimum {min_val}")
elif value > max_val:
return (False, f"{value} is above maximum {max_val}")
else:
return (True, f"{value} is valid ({min_val}-{max_val})")
return validate
age_validator = make_validator(0, 150)
print(age_validator(25)) # (True, '25 is valid (0-150)')
print(age_validator(-5)) # (False, '-5 is below minimum 0')
print(age_validator(200)) # (False, '200 is above maximum 150')
score_validator = make_validator(0, 100)
print(score_validator(85)) # (True, '85 is valid (0-100)')
The min_val and max_val are free variables remembered by the closure.
Scoring Guide
| Score | Level |
|---|---|
| 5/5 | ⭐ Scope & closures mastered — ready for decorators |
| 3-4/5 | 👍 Good grasp — review late binding and nonlocal |
| 1-2/5 | 📚 Revisit LEGB rule, closures, and variable shadowing |
Next chapter: Lambda & Functional Programming →