PCAP study materials
PCAP study resources

The Lazy Closure

PCAP · Put your knowledge to work in context.

All case studies

The Lazy Closure

You are writing a function that creates a list of multiplier functions:

def make_multipliers(n):
    return [lambda x: x * i for i in range(n)]

multipliers = make_multipliers(3)
for m in multipliers:
    print(m(10))

The developer expects the output to be 0, 10, 20. Instead, it prints 20, 20, 20. The developer then tries:

def make_multipliers(n):
    return [lambda x, i=i: x * i for i in range(n)]

Now it prints 0, 10, 20 as expected. The developer is confused about why the first version fails and why the second works.

1. Explain why the first version prints `20` three times. What is the role of late binding in this behavior?

Read the worked answer

In the first version, the lambda functions capture the variable i by reference, not by value. All lambdas share the same i from the enclosing scope of make_multipliers. The list comprehension iterates i from 0 to 2, but the lambdas are not called until after the comprehension completes. By that time, i has the value 2. So when each lambda is called with x=10, it computes 10 * 2 = 20. This is known as late binding: the value of i is looked up at call time, not at definition time. The second version uses a default argument i=i, which captures the current value of i at the time each lambda is defined. Default arguments are evaluated at definition time, so each lambda gets its own copy of i (0, 1, 2). Thus the outputs are 0, 10, 20.

2. If you wanted to avoid the default argument trick and instead use a factory function, how would you rewrite `make_multipliers` to achieve the same correct output? Provide code and explain why it works.

Read the worked answer

You can use a factory function that returns a lambda, like this:

def make_multipliers(n):
    def multiplier(i):
        return lambda x: x * i
    return [multiplier(i) for i in range(n)]

Here, each call to multiplier(i) creates a new scope with its own i parameter. The lambda closes over that specific i from the multiplier function's local scope. Since each multiplier call has a distinct i, the lambdas capture the correct values. This works because each invocation of multiplier creates a new closure with its own binding for i, avoiding the shared variable issue.