Cheese icon

The Great Python Lambda Loop Puzzle

Author: guiferviz

Created:

Last Modified:

Here is a very entertaining Decode The Code puzzle to analyze.

funs = []

for i in range(4):
    funs.append(lambda: i)

for i in funs:
    print(i())

What could be the result of such an innocent code? Or should I call it diabolical? GPT-o1, the most advanced model from OpenAI to date, has not been able to provide me with a valid explanation, even after showing it the output of the code. Will we, mere humans, be able to do better than an LLM?

Output Of The Code

Expand the following box to see the output of running the code above.

And the output is…
<function <lambda> at 0x108d0feb0>
<function <lambda> at 0x108e3f2e0>
<function <lambda> at 0x108e3f370>
<function <lambda> at 0x108e3f400>

Four different lambda functions are being displayed on the screen. Everything seems to indicate that the second loop is actually printing the functions themselves and not the result of calling the lambda functions. How is that possible?

Moreover, if we call funs[0]() after executing the second loop, we will not get the first lambda function (0x108d0feb0 in my example), but rather the last one (0x108e3f400).

Explaining the Behavior

In the Python documentation, there is a section called Why do lambdas defined in a loop with different values all return the same result? on which this problem is based; this problem is indeed an extension of it. If we remove the second loop and simply call the first function from the list, we find that instead of getting a 0 as we might expect, it returns a 3.

funs = []

for i in range(4):
    funs.append(lambda: i)

assert funs[0]() == 0  # AssertionError! 3 != 0

As clearly explained in the previously provided link, i is not local to the lambdas; it is declared in an outer scope. Furthermore, the value of i returned by the different lambda functions is accessed when the function is called. Therefore, by the end of the loop, i = 3, which is what it returns. All the four functions in funs are returning 3.

By adding a second loop that uses i again in the same scope, what is achieved is that each call to i() actually returns itself.

Variations

Variation 1 - A Show Function

Now that we understand it, what is this other evilish variation going to output?

funs = []

for i in range(4):
    funs.append(lambda: i)

def show():
    for i in funs:
        print(i())

show()
What’s the output?
The output generated by the code above is:
3
3
3
3

The i inside show is a local variable, so it does not replace the value of the global i, hence it returns the last value of the global i, which is 3.

Variation 2 - Define And Show Functions

What about?

funs = []

def define():
    for i in range(4):
        funs.append(lambda: i)

def show():
    for i in funs:
        print(i())

define()
show()
What’s the output?
The output generated by the code above is:
3
3
3
3

Quite similar to the previous example, i in show is local and does not overwrite the value of the i in define. Unlike in the previous example, the i used in the lambda functions is local to define instead of being global. For practical purposes, this makes no difference; however, from Python’s perspective, lambda functions are implemented with a Closure. This can be verified with the following show function.

def show():
   for i in funs:
       print(i())
       assert i.__closure__ is not None

In previous examples, a closure was not needed (Python `__closure__` Dunder was None) as the i variable was in the global context; now that i is local to define, Python somehow needs to keep a reference to it in order to get its value when the lambda functions are executed.

Fixing the Issue

How can we modify the lambda function to return the value of i at the time of its definition, allowing us to obtain values from 0 to 3 when calling the functions in funs sequentially?

One solution is to add a parameter k, to the lambda function that defaults to the value of i at that iteration.

funs = []

for i in range(4):
    funs.append(lambda k=i: k)

for i in funs:
    print(i())

# Output:
# 0
# 1
# 2
# 3

A function like lambda i=i: i would also work, but makes it a bit more difficult to understand.