lambda: the same thing as def except for two — and the famous loop bug is neither of them
lambda and def share a type, share the bytecode of the body, and have call times measurement cannot tell apart. The difference is the name, and that the body must be an expression. The famous loop bug has nothing to do with lambda: a plain def breaks in exactly the same way.
Full technical treatment
TL;DR
A lambda is an ordinary function written as an expression. It belongs
where the function is small and needed right at the call site — as an argument
to sorted, say — and where giving it a separate name would get in the
reader's way rather than help. The object that comes out is the same one def
makes: same type, same bytecode for the body.
Hence the main consequence: choosing between lambda and def is about
readability, not about behaviour. The differences live in the grammar and the
metadata: a lambda's body has to be an expression, and its name is
synthetic — <lambda> instead of a real one, which is what tracebacks show.
The famous loop bug — [lambda: i for i in range(3)] yielding [2, 2, 2] —
is not about lambda: a plain def in a loop breaks the same way, the FAQ
says so outright, and the reason is that the three functions point at one
cell (verified — there is exactly one, and it holds 2).
Beyond that is what separates knowing from having read. Call times for
def and lambda are indistinguishable: 22.7 ns against 22.7 ns (3.13.7). A
lambda cannot be annotated, and what forbids that is the grammar itself rather
than the "expressions only" rule (PEP 3107). The cost in key=: lambda and
def are indistinguishable (7.97 against 7.98 µs), while operator.itemgetter
is one and a half times faster than either in that measurement. The choice
is not between lambda and def.
- a function is an object like a number or a string: it can be put in a variable and passed to another function as an argument;
- an expression is evaluated and yields a value, while a statement does something:
x + 1is an expression,return xa statement; sorted(data, key=…)is an ordinary call that is handed a function.
- closures, cells,
__closure__, late binding; operator.itemgetter, the walrus:=, type annotations and bytecode.
Base: an ordinary function written as an expression
A function in Python can be passed as an argument, and the standard library
relies on that constantly. sorted, min, max and filter take a function
because what they need is not a finished algorithm but a rule: which value to
compare by, what counts as a match.
Usually such a function is declared first and passed afterwards:
def second(pair):
return pair[1]
rows.sort(key=second)A lambda is a way to write exactly the same function as an expression and
put it where it is needed:
rows.sort(key=lambda pair: pair[1])Both forms produce a function and both do the same thing. The difference is that in the second one the function has no separate name and no separate place in the file.
Which answers the question of when to write it that way. When the function is small, needed in exactly one place — the call itself — and a separate name would get in the way: it makes the reader look away, find the definition and come back, all for something that fitted on half a line.
And the converse rule, from the same reasoning: as soon as a name is needed —
the function is called from several places, it needs a type annotation, its
name should show up in a traceback when it fails — write a def. PEP 8 says
the same thing when it objects to f = lambda ...: there the name appears to
exist, yet the traceback does not have it.
That is already enough to answer the basic interview question: a lambda is
the same function written as an expression, and it belongs where a separate
name would hurt readability. Everything below is about how exactly lambda
differs from def (there are few differences, and speed is not among them),
about the famous loop bug that is not about lambda, and about what the
measurements actually show.
Mechanism 1: one type, one bytecode, one timing
lambda produces an object of the same function type a def does. The differences are grammar and metadata.Start with what three lines settle.
def named(x):
return x + 1
anon = lambda x: x + 1type(named) is type(anon) is True; both are function. The bytecode of the
body matches instruction for instruction:
RESUME LOAD_FAST LOAD_CONST BINARY_OP RETURN_VALUE
Call time is 22.7 ns for def against 22.7 ns for lambda on 3.13.7. The gap
is smaller than the run-to-run spread; in repeated runs the order swaps. The
claim is exactly that: there is nothing to measure.
The reference describes lambda in a single line of grammar and says the
expression lambda arguments: expression “yields a function object” — the same
object def makes.
Mechanism 2: difference one — the name
A lambda's __name__ is <lambda>, and that shows up where it matters most.
Two functions that fail identically:
def boom_named(x): return int(x)
boom_anon = lambda x: int(x)
boom_named("test") # ValueError — and the same for boom_anonIn a traceback they look different:
last frame (def): boom_named
last frame (lambda): <lambda>
Hence PEP 8's recommendation, which rests on a reason rather than on taste:
“Always use a def statement instead of an assignment statement that binds a
lambda expression directly to an identifier” — because “the name of the
resulting function object is specifically 'f' instead of the generic
'<lambda>'. This is more useful for tracebacks”.
Note that the rule covers only the f = lambda ... case. Passing a lambda
as an argument — sorted(data, key=lambda x: x[1]) — is not what PEP 8 objects
to: there was never a name to lose.
Mechanism 3: difference two — the body must be an expression
The reference: “lambda forms can only contain expressions, not statements”. Compiling each form shows exactly what does not get through:
| form | result |
|---|---|
lambda x: (x := 1) | compiles — the walrus is an expression |
lambda x: x = 1 | SyntaxError |
lambda x: return x | SyntaxError |
lambda x: raise ValueError | SyntaxError |
lambda x: assert x | SyntaxError |
lambda x -> int: x | SyntaxError — return annotations are not allowed |
The last row is about something else: what forbids the annotation is not the
“expressions only” rule but the grammar of lambda itself, and PEP 3107 says so
outright. In practice it matters most: a lambda cannot be annotated. If a
function needs a type, it is a def, and not for reasons of taste.
Mechanism 4: the error that does not crash
And now code that does not crash, does not warn — and returns the wrong thing.
fns = [lambda: i for i in range(3)]
[f() for f in fns] # [2, 2, 2], not [0, 1, 2]This is not about lambda. The FAQ closes its explanation with a sentence
people tend not to reach: “Note that this behaviour is not peculiar to lambdas,
but applies to regular functions too”. Running it confirms — a loop with a plain
def gives the same [2, 2, 2]:
defs = []
for i in range(3):
def make():
return i
defs.append(make)
[f() for f in defs] # [2, 2, 2] — lambda has nothing to do with itThe reason is visible in memory: the three functions share one closure cell.
A cell is a separate object CPython creates for a variable more than one scope
looks at; a function keeps its references to them in __closure__. Verified —
the set of cell ids has exactly one element, and the cell holds 2.
Outside a teaching example it looks like this: three checks built in a loop all
get one threshold — the last one. [lambda v: v > t for t in (10, 100, 1000)]
passes 3 values instead of 6, with no exception anywhere.
The workaround is a default argument, because it is evaluated when the function is created:
fixed = [lambda i=i: i for i in range(3)] # [0, 1, 2]Such functions have __closure__ of None: there is no closure at all, the
value lives in the function itself.
How a comprehension differs from a loop
There is a detail here the FAQ does not cover: its example uses a for loop,
and the variable behaves differently in a comprehension — one cell either way,
but a different lifetime.
| variable visible afterwards | changing it changes the result | |
|---|---|---|
for x in range(3): … | yes, x == 2 | yes: x = 100 → all three return 100 |
[… for y in range(3)] | no | nothing to change — no y outside |
In a loop the functions hold a reference to a variable that outlives the loop and can be reassigned later. A comprehension has its own scope and leaks nothing — but the three functions still share one cell, and the result is the same.
Deeper: what it actually costs
Three ways to fetch the same key; the table gives the time of one sort of a 200-element list of tuples, best of seven runs of 20,000 sorts each.
| way | 3.13.7 | 3.14.7 |
|---|---|---|
key=lambda x: x[1] | 7.97 µs | 7.67 µs |
key=named (the same def) | 7.98 µs | 7.71 µs |
key=operator.itemgetter(1) | 5.30 µs | 5.40 µs |
Read the columns separately: the builds these numbers come from differ by more than the language version. What to compare is rows within one column.
The first two rows are indistinguishable — as expected: it is the same kind of
object either way. itemgetter is roughly 1.5× faster than either, and that gap
is stable between runs.
The reason is not syntax: itemgetter is a callable written in C, and no
interpreter frame is created per element. The same holds for attrgetter and
methodcaller.
The “one and a half times” has a boundary, and naming it matters more than the number. What was measured is one operation — sorting a 200-element list of tuples by one item; in this lesson's own practice run, on different data and a different machine, the same move gave more than two times. What carries over is not the multiple but the direction: the win comes from a Python-level call disappearing, and its size depends on how much of the time that call took. On a ten-element sort nothing changes at all.
So the practical conclusion is written from the boundary: the "lambda or def"
argument costs nothing either way — the measurement shows no difference
between them. If a sort is genuinely hot, look towards operator, and look
with a measurement on your own data rather than by choosing between two ways of
writing the same thing.
Deeper: version history
| Version | Change | What it means for your code |
|---|---|---|
| 3.0 | PEP 3107 gives annotations to def and withholds them from lambda — a recorded decision rather than an oversight: “Lambda's syntax does not support annotations”. Since then, a function that needs a type has to be a def, and that is enforced by the compiler, not by convention. | |
| 3.8 | PEP 572: := is an expression rather than a statement, which is why it works inside a lambda. It is the only way to “assign” anything in a lambda body: lambda x: (y := x) compiles, lambda x: y = x does not. | |
| 3.12 | Comprehensions are inlined into the calling code (PEP 709), but they kept their own scope: the comprehension variable is still invisible from outside — checked on 3.11, 3.12, 3.13 and 3.14 with the same result. |
How to answer in an interview
The short answer: a lambda is the same function written as an expression.
The object it builds is of the same type as a def's, with the same bytecode
for the body; you write one where the function is needed right at the call site
and a separate name would hurt readability.
That is enough to answer correctly. Beyond it is what you add if the interviewer digs.
If the interviewer digs deeper
First, name the actual differences. There are two, both in grammar and
metadata: the name (<lambda> instead of a real one — which is what shows up
in a traceback) and the requirement that the body be an expression. A third is
grammatical: a lambda cannot be annotated, and it is the grammar that forbids
it, not the "expressions only" rule. Which also frames PEP 8: it objects not to
lambda but to f = lambda ..., because that form takes the name out of the
traceback.
Second, the trap they are waiting for: the famous
[lambda: i for i in range(3)] yielding [2, 2, 2] has nothing to do with
lambda — a plain def in a loop breaks the same way, and the FAQ says so
outright. The reason is that the three functions point at one cell.
Third, if the conversation turns to speed, name the real choice: lambda and
def in key= are indistinguishable (7.97 against 7.98 µs), while
operator.itemgetter is one and a half times faster than either in that
measurement. The multiple belongs to the measurement; the direction is what
carries over — the win comes from a Python-level call disappearing.
Next they ask
If it is the same as def, what is lambda for?
There are exactly two differences, and neither is speed: it has no name of its
own, and its body is an expression rather than a set of statements. The object
type, the bytecode and the time are the same for lambda and def.
The famous lambda-in-a-loop trap — is that about lambda?
No, it is about closures: what is captured is the variable, not the value, and a
def in the same loop behaves identically. lambda only makes the mistake
shorter to write.
Common misconceptions
lambda is faster than def because it is shorter
It is neither faster nor slower: it is an object of the same type. type(named) is type(anon) is True, the body's bytecode matches instruction for instruction, and the call takes 22.7 ns against 22.7 ns on 3.13.7 — inside the spread. In repeated runs the order of those two swaps.
the [lambda: i for i in range(3)] bug is a lambda quirk
The FAQ says the opposite outright: “this behaviour is not peculiar to lambdas, but applies to regular functions too”. Verified: a loop with a plain def gives the same [2, 2, 2]. What happens is that a function closes over a variable rather than a value — and lambda is merely the shortest way to make a function inside a loop.
the value simply did not get saved in time
There was no value to save: what was saved is a reference. The three functions share EXACTLY ONE closure cell — verified by id — and it holds the last value. The lambda i=i: i workaround does not “copy” anything; it works because a default argument is evaluated when the function is created. The fixed variant has __closure__ of None.
PEP 8 bans lambda
One case is banned — binding a lambda to a name: “Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier”. The reason is stated there and is checkable: the traceback will read <lambda> instead of a name. Passing a lambda as an argument is not what PEP 8 objects to.
anything goes in a lambda, just on one line
Nothing that is a statement goes. Verified by compiling: =, return, raise and assert are all SyntaxError; only an expression gets through — including the walrus :=. Separately and importantly: a return annotation is a SyntaxError too, so a typed function must be a def.
key=lambda is the bottleneck of a sort
The bottleneck is not lambda but the fact that Python-level code fetches the key. Measured: key=lambda 7.97 µs, key=def 7.98 (indistinguishable), key=itemgetter(1) 5.30. The 1.5× win in that measurement comes from moving to operator, not from choosing between two ways of writing a function; on other data the multiple differs and the direction does not.
a comprehension variable behaves like a loop variable
Not quite, and the difference shows up while debugging. After for x in range(3) the variable survives outside: x == 2, and writing x = 100 later makes all three functions return 100. A comprehension has its own scope — there is no such variable outside at all. The result is the same either way: [2, 2, 2].
Practice
Two exercises. Answer first, then check against the real output: in both, the right answer comes from a recorded run rather than from an editor.
Practice · predict the output
lambdas = [lambda: i for i in range(3)]
defs = []
for i in range(3):
def made():
return i
defs.append(made)
print([f() for f in lambdas])
print([f() for f in defs])Practice · estimate
Knowledge check
What does [f() for f in [lambda: i for i in range(3)]] return?
What measured this
The numbers in this article come from these scripts. Each one opens from here, together with the record of the run: what it was measured on, what came out, and with what spread.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A
lambdais an ordinary function written as an expression. It belongs where the function is small and needed right at the call site — as an argument tosorted, say — and where giving it a separate name would get in the reader's way rather than help. The object that comes out is the same onedefmakes: same type, same bytecode for the body. - Hence the main consequence: choosing between
lambdaanddefis about readability, not about behaviour. The differences live in the grammar and the metadata: a lambda's body has to be an expression, and its name is synthetic —instead of a real one, which is what tracebacks show. The famous loop bug —[lambda: i for i in range(3)]yielding[2, 2, 2]— **is not about lambda**: a plaindefin a loop breaks the same way, the FAQ says so outright, and the reason is that the three functions point atone cell (verified — there is exactly one, and it holds 2). - Beyond that is what separates knowing from having read. Call times for
defandlambdaare indistinguishable: 22.7 ns against 22.7 ns (3.13.7). A lambda cannot be annotated, and what forbids that is the grammar itself rather than the "expressions only" rule (PEP 3107). The cost inkey=:lambdaanddefare indistinguishable (7.97 against 7.98 µs), whileoperator.itemgetteris one and a half times faster than either in that measurement. The choice is not betweenlambdaanddef.
In fact
- It is neither faster nor slower: it is an object of the same type.
type(named) is type(anon)isTrue, the body's bytecode matches instruction for instruction, and the call takes 22.7 ns against 22.7 ns on 3.13.7 — inside the spread. In repeated runs the order of those two swaps. - The FAQ says the opposite outright: “this behaviour is not peculiar to lambdas, but applies to regular functions too”. Verified: a loop with a plain
defgives the same[2, 2, 2]. What happens is that a function closes over a variable rather than a value — andlambdais merely the shortest way to make a function inside a loop. - There was no value to save: what was saved is a reference. The three functions share EXACTLY ONE closure cell — verified by
id— and it holds the last value. Thelambda i=i: iworkaround does not “copy” anything; it works because a default argument is evaluated when the function is created. The fixed variant has__closure__ofNone. - One case is banned — binding a lambda to a name: “Always use a
defstatement instead of an assignment statement that binds a lambda expression directly to an identifier”. The reason is stated there and is checkable: the traceback will read<lambda>instead of a name. Passing a lambda as an argument is not what PEP 8 objects to. - Nothing that is a statement goes. Verified by compiling:
=,return,raiseandassertare allSyntaxError; only an expression gets through — including the walrus:=. Separately and importantly: a return annotation is aSyntaxErrortoo, so a typed function must be adef. - The bottleneck is not
lambdabut the fact that Python-level code fetches the key. Measured:key=lambda7.97 µs,key=def7.98 (indistinguishable),key=itemgetter(1)5.30. The 1.5× win in that measurement comes from moving tooperator, not from choosing between two ways of writing a function; on other data the multiple differs and the direction does not. - Not quite, and the difference shows up while debugging. After
for x in range(3)the variable survives outside:x == 2, and writingx = 100later makes all three functions return100. A comprehension has its own scope — there is no such variable outside at all. The result is the same either way:[2, 2, 2].
By version
- 3.0
- PEP 3107 gives annotations to
defand withholds them fromlambda— a recorded decision rather than an oversight: “Lambda's syntax does not support annotations”. Since then, a function that needs a type has to be adef, and that is enforced by the compiler, not by convention.< - 3.8
- PEP 572:
:=is an expression rather than a statement, which is why it works inside alambda. It is the only way to “assign” anything in a lambda body:lambda x: (y := x)compiles,lambda x: y = xdoes not.< - 3.12
- Comprehensions are inlined into the calling code (PEP 709), but they kept their own scope: the comprehension variable is still invisible from outside — checked on 3.11, 3.12, 3.13 and 3.14 with the same result.<
What is covered
- Base: an ordinary function written as an expression
- Mechanism 1: one type, one bytecode, one timing
- Mechanism 2: difference one — the name
- Mechanism 3: difference two — the body must be an expression
- Mechanism 4: the error that does not crash
- Deeper: what it actually costs
- Deeper: version history
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
- What measured this
Sources & further reading
6 SOURCES
- The language reference — lambda expressionsOfficial documentation. A one-line grammar: `lambda [varargslist] ": " expression`. The same page carries the two restrictions this lesson checks by compiling: “The lambda's body is restricted to a single expression” and “lambda forms can only contain expressions, not statements”.https://docs.python.org/3.14/reference/expressions.html#lambda
- Programming FAQ — why lambdas defined in a loop return the same resultOfficial documentation. Answers exactly the question this lesson's loop section is about, and ends with the sentence people tend not to reach: “Note that this behaviour is not peculiar to lambdas, but applies to regular functions too”. Verified by running it: a plain def in a loop gives the same result.https://docs.python.org/3.14/faq/programming.html
- PEP 8 — on binding a lambda to a namePEP. “Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier”, with the reason stated right there: “the name of the resulting function object is specifically 'f' instead of the generic '<lambda>'. This is more useful for tracebacks”. Verified: the last traceback frame really does read `<lambda>`.https://peps.python.org/pep-0008/
- PEP 3107 — Function AnnotationsPEP. Collin Winter and Tony Lownds, Final, Python 3.0. The absence of annotations on lambda is a recorded decision, not a side effect: “Lambda's syntax does not support annotations. The syntax of lambda could be changed to support annotations, by requiring parentheses around the parameter list”. Verified by compiling: `lambda x -> int: x` is a SyntaxError on all four versions.https://peps.python.org/pep-3107/
- PEP 572 — Assignment ExpressionsPEP. Chris Angelico, Tim Peters, Guido van Rossum; Final, Python 3.8. The walrus is an EXPRESSION: “The value of such a named expression is the same as the incorporated expression, with the additional side-effect that the target is assigned that value”. Which is why it works inside a lambda while ordinary assignment does not.https://peps.python.org/pep-0572/
- operator — itemgetter and attrgetterOfficial documentation. The module the practical conclusion about `key=` reduces to. Measured: sorting with `itemgetter(1)` is one and a half times faster than with an equivalent `lambda` — and that gap is stable between runs, unlike the gap between `lambda` and `def`.https://docs.python.org/3.14/library/operator.html