I have written a little bit about Lambda Expressions before, although that was in the context of writing Racket. I have also used them occasionally - I honestly haven’t had much need for them - in Python, doing things like sorting based on a specific key:
# This is perhaps a slightly contrived exampleunsorted = [(5,"b"), (1, "c"), (99, "a")]sorted_alphabetically = sorted(unsorted, key=lambda x: x[1])print(sorted_alphabetically)# -> [(99, 'a'), (5, 'b'), (1, 'c')]
The above use of a lambda function is the equivalent of doing this:
In the second example I defined a named function called sort_alpha, and passed it as an argument to sorted. This is all well and good. The syntax can be simplified with inline functionality using the first example, by defining the anonymous lambda function and passing the expression to sorted. Another way to do that (although I don’t know why you would) is to assign the lambda function to a variable1
That’s all well and good, but what happens if we start combining Python features? One might imagine creating a list comprehension that maybe uses a lambda to create something specific. For example, you might naively assume - like I did - that the following would print a the numbers 1 through 10:
l = [lambda x: x + i for i in range(10)]for f in l: print(f(1))# Does _not_ print 1,2...10. WHY?!# Instead, it prints the number 10 over and over
Playing with objects
At a glance that is counter-intuitive. Using a list comprehension is used to create a list, and conditions can be added to that list; I can also uses variables and expressions to build it. I can do something like this, for example:
i = 1test_comprehension = [x + i for x in range(10)]print(test_comprehension)# -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
The question, then, is why does the lambda version of the list comprehension print 10 over and over when passed the argument 1? Looking at it closely, it appears that it is simply using the last value of i it received from the range(10) expression.
To wrap my head around this, it is easier to walk it step by step, decomposing exactly what Python is doing:
l = [lambda x: x + i for i in range(10)]for f in l: print(f(1))
At first, there is an empty list assigned to the variable l. In other words, we have the equivalent of l = []. Then, for each value of i in range(10) (meaning, i=0, i=1 ... i=9 ) the list appends a lambda function object2 (which is specified as lambda x: x + i). Another way to do this is to define and append a function object to a list on each iteration:
e = []for i in range(10): def f(x): return x + 1 e.append(f)
We can confirm the result of this by printing the current values of both l and e (note: I reran the code with range(3) instead of truncate the output a bit):
print(l)print(e)# Output of print(l)[<function <lambda> at 0x7f3ebc5191c0>, <function <lambda> at 0x7f3ebc519260>, <function <lambda> at 0x7f3ebc519300>]# Output of print(e)[<function f at 0x7f3ebc5194e0>, <function f at 0x7f3ebc519440>, <function f at 0x7f3ebc519580>]
Notice that despite all of these functions doing the same thing, they are different objects: each one is located in a different place in memory.
Aside
This is because we are defining a new function object with each iteration. Compare that with:
def f(x): return x + 1p = [f for i in range(3)]print(p)# Output[<function f at 0x7f958fd19620>, <function f at 0x7f958fd19620>, <function f at 0x7f958fd19620>]
Notice how here we still have a list of references to a function f, but all references point to the same location
Playing with scope
So all of that is well and good - the list creates references to different objects. But question remains: why is the final output using the same number? After all, one would imagine that [lamda x: x + i for i in range(10)] would use the value of i and added to each iteration of the lambda. Unfortunately (depending on your perspective, I guess), the actual behaviour is different, and it is tied to the concept of Lexical Scoping.
What happens here is that, as we established, a new lambda object is created for each iteration of i. However, the evaluation of that lambda function happens afterwards, in the second loop where we call the lambda objects:
for f in [list_of_lambdas]: print(f(1))
Here, each lambda is defined as lambda x: x + i, and the variable i is simply a pointer. In other words, each of the lambda expressions knows to use i but doesn’t know what the value of i is. Perhaps more formally, what we have is a closure that “closes” over each of the lambda expressions - meaning the access to the environment where a variable was defined is preserved, but - and this is the critical part - not the state of the variable.
Another important point here is that that reference to i exists only inside of scope of the comprehension3
The result, then, is that when we do
... print(f(1))...
we are still passing the 1 to each of the functions in the list, and they do add it to i - but because this is happening after the comprehension has completed, the value of i points to 9. This is why the result is printing 10 every time.
Footnotes
This is a rather un-Pythonic way of doings things, and defeats the purpose of lambdas in general. Even my linter of choice, ruff, complains about it telling me not to assign a ‘lambda’. ↩
This is the critical point. It does not evaluate the lambda function. It gets a lambda function itself. ↩
Despite what some online articles and all LLMs apparently might tell you. I did not write this using LLMs, but I did use it to ask questions to ensure I understand. Both Claude and ChatGPT are absolutely convinced that you can do a list comprehension, like l = [lambda x: x + i for i in range(3)] and then print(i) afterwards. Apparently they do not understand how Python 3 scopes work (and I don’t know how Python 2 works, so maybe they are correct in that regard, but frankly if you are learning these things I would guess you are using Python 3 or you already know how these things work) ↩