The issue

Recently, a colleague posted the following code snippet and asked for an explanation of why it didn’t work as they expected:

x = []
for i in range(1, 10):
    x.append(lambda z: "%d" % i + z)
print(list(x[i]("banzaii") for i in range(0, 9)))

which results in:

['9banzaii', '9banzaii', '9banzaii', '9banzaii', '9banzaii', '9banzaii', '9banzaii', '9banzaii', '9banzaii']

instead of the expected:

['1banzaii', '2banzaii', '3banzaii', '4banzaii', '5banzaii', '6banzaii', '7banzaii', '8banzaii', '9banzaii']

The explanation

The key is that the variable i is outside of the scope of the created lambda. Only the variable z is in the lambda scope. Essentially it would be like writing:

def fn(z):
    return "%d" % i + z

x = []
for i in range(1, 10):
    x.append(fn)

which makes it a lot more obvious what is going on. The global variable i is used each time the function is actually called–which is after the for loop is executed and i is now 9.

The fix

The fix is simple: make sure that i is in the scope of the function that is appended to array x.

Solution 1: using a closure

def makefunc(i):
    def inner(z):
        return "%d" % i + z
    return inner

x=[]
for i in range(1, 10):
    x.append(makefunc(i))

In this example, a closure around the function inner passes i into the returned function’s scope.

Solution 2: using lambda to create an anonymous closure

x = []
for i in range(1, 10):
    x.append((lambda n: lambda z: "%d" % n + z)(i))

Here, we are appending the result of calling a lambda function with the argument i which returns a lambda that uses that value. This is exactly equivalent to the previous example, but for people who like one-liners. :)

Solution 3: using a lambda with a default argument

x = []
for i in range(1, 10):
    x.append(lambda z, n=i: "%d" % n + z)

This solution ensures that we are using a variable n that is in the lambda’s scope and is initialized by default to i. A possible issue with this solution is if the caller of the function in x[i] passes an additional argument which overrides the default value of n. With the previous solutions, this bug would raise a TypeError exception instead of running successfully with (most likely) unintended results.