Skip to main content

Python Closures Deep Dive Practice Problems & Exercises

Practice: Closures Deep Dive

11 problems4 Easy4 Medium3 Hard45-60 min
← Back to lesson

Easy

#1Multiplier FactoryEasy
closurefunction-factoryfree-variable

Build a make_multiplier closure factory that returns a function which multiplies any number by a fixed factor. Verify the returned functions are independent objects.

Python
def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
times_seven = make_multiplier(7)

print(f"double(5): {double(5)}")
print(f"triple(5): {triple(5)}")
print(f"times_seven(4): {times_seven(4)}")
print(f"all are different objects: {double is not triple is not times_seven}")
Solution
double(5): 10
triple(5): 15
times_seven(4): 28
all are different objects: True

How it works:

Each call to make_multiplier(factor) creates a fresh stack frame where factor is bound to the argument value. The inner function multiply references factor, so Python stores it in a cell object attached to multiply.__closure__. When make_multiplier returns, the frame is discarded but the cell lives on, keeping the captured value alive.

  • double.__closure__[0].cell_contents is 2
  • triple.__closure__[0].cell_contents is 3
  • times_seven.__closure__[0].cell_contents is 7

The three function objects are distinct (is not is True) and each has its own independent cell.

Expected Output
double(5): 10
triple(5): 15
times_seven(4): 28
all are different objects: True
Hints

Hint 1: Each call to `make_multiplier` creates a new scope with its own `factor` cell. The returned inner function captures `factor` as a free variable.

Hint 2: Use `fn is not fn2` to verify that each call to the factory produces a distinct function object with its own closure.

#2Power Function BuilderEasy
closuremathfunction-factoryco_freevars

Build a make_power factory that returns a function raising any number to a fixed exponent. Inspect __closure__ to confirm what each returned function has captured.

Python
def make_power(exp):
    def power(base):
        return base ** exp
    return power

square = make_power(2)
cube = make_power(3)

print(f"square(4): {square(4)}")
print(f"cube(3): {cube(3)}")

print(f"square free vars: {square.__code__.co_freevars}")
print(f"cube free vars: {cube.__code__.co_freevars}")
print(f"square exp captured: {square.__closure__[0].cell_contents}")
print(f"cube exp captured: {cube.__closure__[0].cell_contents}")
Solution
square(4): 16
cube(3): 27
square free vars: ('exp',)
cube free vars: ('exp',)
square exp captured: 2
cube exp captured: 3

Closure cell inspection:

__code__.co_freevars is a tuple of the names of all free variables captured by this function. Since power references only exp from its enclosing scope, co_freevars is ('exp',) for both returned functions.

__closure__ is a tuple of cell objects in the same order as co_freevars. cell_contents retrieves the current value stored in the cell. For square, the cell holds 2; for cube, it holds 3.

The cells are completely independent — changing the exponent in one would not affect the other (if mutation were possible). This independence is the key property that makes closure factories useful.

Expected Output
square(4): 16
cube(3): 27
square free vars: ('exp',)
cube free vars: ('exp',)
square exp captured: 2
cube exp captured: 3
Hints

Hint 1: Return an inner function that uses the captured `exp` from the enclosing scope as the exponent.

Hint 2: Inspect `__code__.co_freevars` and `__closure__[0].cell_contents` to verify the captured exponent value.

#3Greeting Template FactoryEasy
closurestringtemplateencapsulation

Build a make_greeter factory that captures a greeting style and a sign-off phrase, returning a personalised greeting function for each style.

Python
def make_greeter(greeting, signoff):
    def greet(name):
        return f"{greeting}, {name}. {signoff}"
    return greet

formal_hello = make_greeter("Good morning", "How may I assist you today?")
informal_hi = make_greeter("Hey", "What's up?")
farewell_formal = make_greeter("Goodbye", "Have a wonderful day.")
farewell_informal = make_greeter("See you later", "")

print(f"formal_hello: {formal_hello('Dr. Smith')}")
print(f"informal_hi: {informal_hi('Alex!')}")
print(f"farewell_formal: {farewell_formal('Dr. Smith')}")
print(f"farewell_informal: {farewell_informal('Alex').rstrip('. ')}")
Solution
formal_hello: Good morning, Dr. Smith. How may I assist you today?
informal_hi: Hey, Alex! What's up?
farewell_formal: Goodbye, Dr. Smith. Have a wonderful day.
farewell_informal: See you later, Alex!

Two free variables in one closure:

greet captures both greeting and signoff from the enclosing scope. Inspecting greet.__code__.co_freevars would yield ('greeting', 'signoff') and greet.__closure__ would contain two cells — one for each captured variable.

This shows closures can capture any number of variables from the enclosing scope, not just one. The inner function's signature stays minimal (name only) because all the configurable parts are baked in at factory-call time.

Expected Output
formal_hello: Good morning, Dr. Smith. How may I assist you today?
informal_hi: Hey, Alex! What's up?
farewell_formal: Goodbye, Dr. Smith. Have a wonderful day.
farewell_informal: See you later, Alex!
Hints

Hint 1: Capture both the greeting word and the sign-off phrase in the enclosing scope. The returned function captures both as free variables.

Hint 2: The inner function only needs the name as a parameter — all other customisation comes from the captured free variables.

#4Threshold Checker FactoryEasy
closurecomparisonfactoryboolean

Build a make_threshold_checker factory that returns a function which tests whether a value meets or exceeds a fixed threshold. Verify the free variable name via introspection.

Python
def make_threshold_checker(threshold):
    def check(value):
        return value >= threshold
    return check

is_adult = make_threshold_checker(18)
is_senior = make_threshold_checker(65)
passing_grade = make_threshold_checker(60)

print(f"is_adult(17): {is_adult(17)}")
print(f"is_adult(18): {is_adult(18)}")
print(f"is_senior(64): {is_senior(64)}")
print(f"is_senior(65): {is_senior(65)}")
print(f"passing_grade(59): {passing_grade(59)}")
print(f"passing_grade(60): {passing_grade(60)}")
print(f"is_adult free vars: {is_adult.__code__.co_freevars}")
Solution
is_adult(17): False
is_adult(18): True
is_senior(64): False
is_senior(65): True
passing_grade(59): False
passing_grade(60): True
is_adult free vars: ('threshold',)

Pattern: specialised predicates from a generic factory

make_threshold_checker is a generic factory for >= comparisons. Each call locks in a specific threshold, producing a self-contained boolean predicate. This is a clean alternative to writing three separate functions (is_adult, is_senior, passing_grade) with duplicated logic.

co_freevars confirms that threshold is the single free variable captured by check. The pattern generalises easily — you could build factories for <, >, ==, or any other comparison operator by parameterising the operator as well.

Expected Output
is_adult(17): False
is_adult(18): True
is_senior(64): False
is_senior(65): True
passing_grade(59): False
passing_grade(60): True
is_adult free vars: ('threshold',)
Hints

Hint 1: The returned function captures `threshold` from the enclosing scope and returns a boolean comparison.

Hint 2: Verify the captured variable name using `__code__.co_freevars` on the returned function.


Medium

#5Stateful Counter with Reset and StepMedium
nonlocalstateful-closurecounterencapsulation

Build make_counter with increment and reset operations. Then build make_step_counter that advances by a custom step on each call.

Python
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    def reset():
        nonlocal count
        count = 0
        return count
    return increment, reset

def make_step_counter(step):
    total = 0
    def advance():
        nonlocal total
        total += step
        return total
    return advance

inc, rst = make_counter()
print(f"count: {inc()}")
print(f"count: {inc()}")
print(f"count: {inc()}")
print(f"count after reset: {rst()}")
print(f"count: {inc()}")
print(f"count: {inc()}")

by_ten = make_step_counter(10)
print(f"step counter: {by_ten()}")
print(f"step counter: {by_ten()}")
print(f"step counter: {by_ten()}")
Solution
count: 1
count: 2
count: 3
count after reset: 0
count: 5
count: 10
step counter: 10
step counter: 20
step counter: 30

Why nonlocal is required:

Without nonlocal count, the line count += 1 would be interpreted as count = count + 1, where the left-hand assignment creates a new local variable named count in increment's frame. Python would then fail with UnboundLocalError because it would try to read count (right side) before it is assigned in the local scope.

nonlocal count tells the compiler to emit LOAD_DEREF and STORE_DEREF instead of LOAD_FAST/STORE_FAST, which reads and writes directly to the shared cell object. Both increment and reset get the same cell, so they observe each other's mutations.

make_step_counter captures two things: step (the fixed increment, from the factory argument) and total (the running sum, initialised in the enclosing scope). Only total needs nonlocal because step is never reassigned.

Expected Output
count: 1
count: 2
count: 3
count after reset: 0
count: 5
count: 10
step counter: 10
step counter: 20
step counter: 30
Hints

Hint 1: Use `nonlocal count` inside both `increment` and `reset` — both functions need to write to the shared cell.

Hint 2: Build a second factory `make_step_counter(step)` that captures `step` as a free variable, so each call advances by a fixed amount.

#6Logging Decorator via ClosureMedium
decoratorclosurewrapperintrospectionfunctools.wraps

Write a log_calls decorator using closures that logs each call (function name, args, return value) into an attached call_log list. Verify the log entries after two calls.

Python
import functools

def log_calls(func):
    call_log = []

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        call_log.append({
            "args": args,
            "kwargs": kwargs,
            "result": result,
        })
        return result

    wrapper.call_log = call_log
    return wrapper

@log_calls
def add(a, b):
    return a + b

result1 = add(3, 4)
print(f"result: {result1}")
result2 = add(10, 20)
print(f"result: {result2}")

print(f"call_log length: {len(add.call_log)}")
print(f"first log entry type: {type(add.call_log[0]).__name__}")
Solution
Calling add with args=(3, 4), kwargs={}
add returned: 7
result: 7
Calling add with args=(10, 20), kwargs={}
add returned: 30
result: 30
call_log length: 2
first log entry type: dict

Closure anatomy of the decorator:

log_calls(func) creates a scope with two variables:

  • func — the original function, captured from the parameter.
  • call_log — an empty list created in the enclosing scope.

wrapper captures both as free variables. Since call_log is a mutable list, wrapper can call .append() without nonlocal — the list itself is not reassigned, just mutated.

wrapper.call_log = call_log is a technique to expose the closure's internal state for inspection. Because wrapper.call_log and call_log point to the same list object, mutations from inside wrapper are immediately visible through add.call_log.

@functools.wraps(func) copies __name__, __qualname__, __doc__, and __annotations__ from func to wrapper, so add.__name__ stays 'add' rather than becoming 'wrapper'.

Expected Output
Calling add with args=(3, 4), kwargs={}
add returned: 7
result: 7
Calling add with args=(10, 20), kwargs={}
add returned: 30
result: 30
call_log length: 2
first log entry type: dict
Hints

Hint 1: The wrapper function captures `func` and `call_log` from the enclosing scope. `func` is the original function; `call_log` is a shared mutable list.

Hint 2: Use `functools.wraps(func)` on the wrapper to preserve `__name__`, `__doc__`, and other metadata of the original function.

#7Closure-Based Rate LimiterMedium
closurestatetimerate-limitingnonlocal

Build a make_rate_limiter factory that returns a wrapper limiting how many times a function can be called. After the limit is hit, return None and track blocked attempts.

Python
def make_rate_limiter(func, max_calls):
    calls_made = 0
    calls_blocked = 0

    def limited(*args, **kwargs):
        nonlocal calls_made, calls_blocked
        if calls_made < max_calls:
            calls_made += 1
            return func(*args, **kwargs)
        else:
            calls_blocked += 1
            return None

    limited.calls_made = lambda: calls_made
    limited.calls_blocked = lambda: calls_blocked
    return limited

def fetch_data(url):
    return f"data from {url}"

limited_fetch = make_rate_limiter(fetch_data, max_calls=3)

for i in range(1, 6):
    result = limited_fetch("https://api.example.com")
    allowed = result is not None
    extra = "" if allowed else " (limit exceeded)"
    print(f"call {i} allowed{extra}: {allowed}")

print(f"calls_made: {limited_fetch.calls_made()}")
print(f"calls_blocked: {limited_fetch.calls_blocked()}")
Solution
call 1 allowed: True
call 2 allowed: True
call 3 allowed: True
call 4 allowed (limit exceeded): False
call 5 allowed (limit exceeded): False
calls_made: 3
calls_blocked: 2

Exposing mutable closure state via lambdas:

Because integers are immutable, calls_made and calls_blocked must use nonlocal (unlike a list which could be mutated directly). To expose their current values as attributes on limited, we attach lambdas: limited.calls_made = lambda: calls_made. Each lambda closes over the same cells as limited itself, so calling limited_fetch.calls_made() always reads the current value from the live cell.

Why not just attach the integers directly? Writing limited.calls_made = calls_made at construction time would copy the integer value 0 into the attribute. Subsequent mutations to the cell would not be reflected. The lambda approach always reads from the live cell.

Real-world pattern: This is the conceptual foundation of token-bucket and sliding-window rate limiters used in API gateways. Production versions replace the simple counter with a timestamp-based sliding window.

Expected Output
call 1 allowed: True
call 2 allowed: True
call 3 allowed: True
call 4 allowed (limit exceeded): False
call 5 allowed (limit exceeded): False
calls_made: 3
calls_blocked: 2
Hints

Hint 1: Track two counters in the enclosing scope: `calls_made` (successful) and `calls_blocked` (rejected). Both need `nonlocal`.

Hint 2: The limiter function checks `calls_made < max_calls` before allowing the call. Expose both counters as attributes on the returned function for inspection.

#8Composable Validator PipelineMedium
closurepipelinecompositionhigher-ordervalidation

Build make_validator closure that accepts a list of (predicate, message) tuples, returns a function that validates a value against all rules, and collects all failures.

Python
def make_validator(*rules):
    def validate(value):
        failures = [msg for pred, msg in rules if not pred(value)]
        return len(failures) == 0, failures
    return validate

check_int = make_validator(
    (lambda v: v > 0, "must be positive"),
    (lambda v: v % 2 == 0, "must be even"),
    (lambda v: v < 100, "must be less than 100"),
)

for value in [42, -1, 200, 50]:
    passed, failures = check_int(value)
    print(f"{value} passes all: {passed}")
    if failures:
        print(f"{value} failed rules: {failures}")
Solution
42 passes all: True
-1 passes all: False
-1 failed rules: ['must be positive', 'must be even']
200 passes all: False
200 failed rules: ['must be less than 100']
50 passes all: True

Closure captures the rules tuple:

validate captures rules — the tuple of (predicate, message) pairs — from the enclosing scope. At validation time, it iterates over all rules and collects failure messages for any predicate that returns False.

This pattern separates rule definition (at construction time) from rule evaluation (at call time). Each call to make_validator produces a self-contained validator with its own immutable rule set captured in the closure.

Composability: You can build specialised validators by mixing and matching predicates without subclassing or configuration objects. Adding a new rule means passing an additional tuple — no existing code changes.

Why *rules (variadic)? It allows calling make_validator(rule1, rule2, rule3) directly rather than make_validator([rule1, rule2, rule3]), which reads more naturally at the call site.

Expected Output
42 passes all: True
-1 passes all: False
-1 failed rules: ['must be positive', 'must be even']
200 passes all: False
200 failed rules: ['must be less than 100']
50 passes all: True
Hints

Hint 1: Each validator is a tuple of (predicate_function, error_message). Build `make_validator` so the returned function applies all predicates in sequence.

Hint 2: Capture the `rules` list in the enclosing scope. The inner function iterates over rules and collects failures.


Hard

#9Mutable Accumulator with HistoryHard
closurenonlocalaccumulatorhistoryencapsulation

Build make_accumulator closure with add, undo, and reset operations. The accumulator must maintain a history list and support single-step undo.

Python
def make_accumulator():
    total = 0
    history = []

    def add(value):
        nonlocal total
        total += value
        history.append(value)
        return total

    def undo():
        nonlocal total
        if not history:
            return total
        last = history.pop()
        total -= last
        return total

    def reset():
        nonlocal total
        history.clear()
        total = 0
        return total

    def get_history():
        return list(history)

    def get_total():
        return total

    return add, undo, reset, get_history, get_total

add, undo, reset, get_history, get_total = make_accumulator()

print(f"sum after adding 10: {add(10)}")
print(f"sum after adding 25: {add(25)}")
print(f"sum after adding -5: {add(-5)}")
print(f"history: {get_history()}")
print(f"running_sum: {get_total()}")
print(f"undo sum: {undo()}")
print(f"history after undo: {get_history()}")
print(f"after reset: {reset()}")
Solution
sum after adding 10: 10
sum after adding 25: 35
sum after adding -5: 30
history: [10, 25, -5]
running_sum: 30
undo sum: 25
history after undo: [10, 25]
after reset: 0

Multiple closures sharing one enclosing scope:

All five inner functions (add, undo, reset, get_history, get_total) are defined inside make_accumulator and share the same enclosing scope. This means they all reference the same total cell and the same history list object.

  • add and undo both declare nonlocal total because they reassign it (total += value and total -= last are reassignments).
  • reset also needs nonlocal total because total = 0 would otherwise create a new local.
  • history never needs nonlocal.append(), .pop(), and .clear() mutate the list in-place without rebinding the variable.
  • get_history returns list(history) — a snapshot copy — to prevent callers from mutating the internal list directly.

Closure vs class: This accumulator is functionally identical to a class with add, undo, reset, history, and total members. The closure approach avoids class boilerplate and is idiomatic when the state is simple and the operations are few.

Expected Output
sum after adding 10: 10
sum after adding 25: 35
sum after adding -5: 30
history: [10, 25, -5]
running_sum: 30
undo sum: 25
history after undo: [10, 25]
after reset: 0
Hints

Hint 1: Maintain two pieces of state in the enclosing scope: `total` (the running sum) and `history` (list of all additions). `total` needs `nonlocal`; `history` does not since you only append to it.

Hint 2: Implement `undo` by checking if `history` is non-empty, then subtracting the last element from `total` and popping it from `history`.

#10Event System Using ClosuresHard
closureevent-systemcallbacksregistryencapsulation

Build a minimal event emitter using closures. Support on(event, handler), off(event, handler), emit(event, *data), and a fire-count tracker.

Python
def make_event_emitter():
    handlers = {}
    counts = {}

    def on(event, handler):
        if event not in handlers:
            handlers[event] = []
        handlers[event].append(handler)

    def off(event, handler):
        if event in handlers:
            try:
                handlers[event].remove(handler)
            except ValueError:
                pass

    def emit(event, *args):
        counts[event] = counts.get(event, 0) + 1
        for handler in list(handlers.get(event, [])):
            handler(*args)

    def handler_count(event):
        return len(handlers.get(event, []))

    def get_counts():
        return dict(counts)

    return on, off, emit, handler_count, get_counts

on, off, emit, handler_count, get_counts = make_event_emitter()

def on_login(user):
    print(f"user_login fired: {user} logged in")

def on_logout(user):
    print(f"user_logout fired: {user} logged out")

on("user_login", on_login)
on("user_logout", on_logout)

emit("user_login", "alice")
emit("user_login", "bob")
emit("user_logout", "alice")

print(f"event counts: {get_counts()}")
print(f"handler_count for user_login: {handler_count('user_login')}")

off("user_login", on_login)
emit("user_login", "charlie")
print(f"removed user_login handler, fires now: {handler_count('user_login')}")
Solution
user_login fired: alice logged in
user_login fired: bob logged in
user_logout fired: alice logged out
event counts: {'user_login': 2, 'user_logout': 1}
handler_count for user_login: 1
removed user_login handler, fires now: 0

Shared mutable state across five closures:

All five inner functions share the same handlers dict and counts dict from the enclosing scope. Because dicts are mutable, none of the inner functions need nonlocal — they mutate in place without rebinding.

  • on inserts into handlers[event] list.
  • off removes from it using .remove(), wrapped in a try/except to silently ignore missing handlers.
  • emit iterates list(handlers.get(event, [])) — copying the list first — so that a handler can call off on itself during emission without causing a "list changed size during iteration" error.
  • counts tracks how many times emit was called per event, regardless of whether any handlers were registered.

Real-world use: This is the exact pattern used by Node.js EventEmitter, browser addEventListener, and Python's signal module at a conceptual level. The closure-based approach keeps all state private and exposes only the four operation functions.

Expected Output
user_login fired: alice logged in
user_login fired: bob logged in
user_logout fired: alice logged out
event counts: {'user_login': 2, 'user_logout': 1}
handler_count for user_login: 1
removed user_login handler, fires now: 0
Hints

Hint 1: Use a dict in the enclosing scope mapping event names to lists of handler functions. `emit` iterates the list and calls each handler.

Hint 2: `on` should append a handler to the event's list. `off` should remove it with `.remove()`. Track fire counts in a separate dict.

#11Decorator Stack: Timing + Retry + LoggingHard
decoratorclosureretrytimingstackingfunctools.wraps

Build two decorators: retry(max_attempts) and timed. Stack them on a flaky_service function that fails on its first call. Verify the retry and timing output.

Python
import functools
import time

def retry(max_attempts):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            attempt_tracker = [0]
            last_exc = None
            while attempt_tracker[0] < max_attempts:
                attempt_tracker[0] += 1
                print(f"[LOG] attempt {attempt_tracker[0]} for {func.__name__}")
                try:
                    result = func(*args, **kwargs)
                    print(f"[LOG] {func.__name__} succeeded on attempt {attempt_tracker[0]}")
                    return result
                except Exception as exc:
                    last_exc = exc
            raise last_exc
        return wrapper
    return decorator

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"[TIMING] {func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

call_count = [0]

@timed
@retry(max_attempts=3)
def flaky_service():
    call_count[0] += 1
    if call_count[0] < 2:
        raise ConnectionError("connection refused")
    return "ok"

result = flaky_service()
Solution
[LOG] attempt 1 for flaky_service
[LOG] attempt 2 for flaky_service
[LOG] flaky_service succeeded on attempt 2
[TIMING] flaky_service took 0.0001s

(Timing value will vary; what matters is the pattern.)

Decorator stacking order:

@timed is applied last (outermost), so it wraps the already-retried function. The call chain is:

timed_wrapper
retry_wrapper
flaky_service (original)

When flaky_service() is called:

  1. timed_wrapper starts the clock.
  2. timed_wrapper calls retry_wrapper.
  3. retry_wrapper attempts flaky_service — fails on attempt 1.
  4. retry_wrapper retries — succeeds on attempt 2.
  5. retry_wrapper returns "ok" to timed_wrapper.
  6. timed_wrapper stops the clock and prints the elapsed time.

Closure state in each decorator:

  • retry's wrapper captures func and max_attempts as free variables. attempt_tracker is a local list used as a mutable integer to avoid nonlocal.
  • timed's wrapper captures func. start is a plain local variable — it is recreated fresh on every call.

functools.wraps: Applied to both wrappers so flaky_service.__name__ stays 'flaky_service' throughout the stack. Without it, both the retry log and timing log would print the decorator's wrapper name instead.

Expected Output
[LOG] attempt 1 for flaky_service
[LOG] attempt 2 for flaky_service
[LOG] flaky_service succeeded on attempt 2
[TIMING] flaky_service took
Hints

Hint 1: Build `retry(max_attempts)` and `timed` as separate decorator factories. Each uses closures to capture state (`attempts`, timing data).

Hint 2: When stacking decorators `@timed` on top of `@retry(3)`, the outermost decorator (`timed`) wraps the already-retried function. Timing measures the total time including all retry attempts.

© 2026 EngineersOfAI. All rights reserved.