Python Basics — Scope & Closures Practice Exercises

3 minute read

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 →

Updated: