Why do all these functions return the same value?

A closure captures the variable, not the value it had when the function was defined. All three lambdas share one cell holding i, and they read it when called — by which time the loop has finished and left it at 2. Bind a value with a default argument, or make a new scope with a factory.

Python semanticsConceptualMedium

Step through it

What to watch

  • The three functions share one cell — that is why they agree.
  • lambda i=i: i captures a value because defaults are evaluated at definition time.
  • The factory version has three separate cells, each with its own value.

Say this out loud

"Python closures are late-binding: they look the variable up at call time, not definition time. So every lambda in that loop sees the final value. I fix it with a default argument, lambda i=i: i, which evaluates i when the lambda is created, or with a factory function so each closure gets its own scope."

Overview

A closure remembers the surrounding scope, but Python resolves a captured local name when the function is called. Functions created in a loop can therefore all see the loop’s final value unless you bind each value explicitly.

Run it

The shared cell, both fixes, and the same bug with def instead of lambda so it is clear the syntax is not the cause.

The shared cell, read straight off the functions.

1Python
Output

The two fixes, and what each one actually does.

2Python
Output

It is not about lambda - and called inside the loop, late binding is what you wanted.

3Python
Output

Capturing a name, not a value

When a nested function refers to a variable from an enclosing scope, Python does not copy the value in. It stores a reference to a cell — a small box holding that variable — and reads the cell when the function runs. You can see them: f.__closure__ is a tuple of cells, and .cell_contents is what each holds right now.

A loop body is not a new scope in Python, so every iteration refers to the same i and therefore the same cell. Three lambdas, one cell, one answer. This is not a quirk of lambda: a nested def in the same loop behaves identically.

The two fixes, and what they really do

A default argument. lambda i=i: i works because default expressions are evaluated when the function object is created, so the current value is captured and stored on the function. It is the shortest fix and it changes the signature, which means a caller can override it — usually harmless, occasionally surprising.

A factory. def make(i): return lambda: i gives each closure a genuinely separate scope, because each call to make creates its own frame and its own cell. It is more code and it is the version that scales to closing over several variables. functools.partial(op, i) is the same idea from the standard library.

Why late binding is the right default

It is easy to read this as a design mistake. It is the behaviour that makes ordinary closures work: a function that closes over self, a counter, or a configuration dictionary is supposed to see the current value, not a snapshot from whenever it was defined. Mutual recursion between two nested functions relies on it, and so does any callback that reads state updated after it was registered.

The loop case is the one place where you wanted a snapshot and the language gave you a reference. Every other closure in your program is benefiting from the same rule.

Where it bites in real code

Callbacks registered in a loop — buttons, handlers, retry functions — all firing with the last item. Tasks created in a loop and appended to a list. And functools.reduce-style pipelines built by stacking lambdas over a loop variable.

The tell is always the same: several functions built in a loop, called after it. If they are called inside the loop, late binding reads the value you expected and the bug never appears — which is why this survives testing so well.

Edge cases to raise

Volunteering these is most of what separates a correct answer from a good one.

The cell is live, not a snapshot. Rebinding the variable after the functions are built changes what they all return - which is occasionally the feature and usually the bug.

The default-argument fix changes the signature. A caller can pass their own value and override the captured one. Harmless in a loop of throwaway callbacks, worth avoiding in a public API.

Comprehensions have their own scope, but the loop variable is still shared by closures built inside them - the scope stops the name leaking out, it does not give each iteration its own cell.

Sources and next steps

How the code works

The shared cell, both fixes, and the same bug with def instead of lambda so it is clear the syntax is not the cause.

How the code works

  1. late[0].__closure__[0] is late[1].__closure__[0]True. This is the whole explanation in one line: there is one box, and all three functions read it.
  2. lambda i=i: iThe right-hand i is evaluated now; the left-hand one is a parameter name. The value is stored on the function, so __closure__ is None — there is nothing to close over any more.
  3. [f.__closure__[0].cell_contents for f in factory][0, 1, 2]. Three calls to make made three frames, so there are three cells to read.
  4. [(lambda: i)() for i in range(3)][0, 1, 2], because each is called before the loop moves on. Late binding only hurts when the call is deferred, which is why the bug survives casual testing.

Change one thing

  • Rebind i = 99 after building late and call them again. They all return 99 — the cell is live, not a snapshot.
  • Replace the factory with functools.partial(lambda x: x, i) and check it gives [0, 1, 2] too. Same idea, different spelling.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What do the three lambdas in [lambda: i for i in range(3)] return?

  2. A Python closure captures:

  3. Why does lambda i=i: i fix it?

  4. Is this specific to lambda?

Cheat sheet

Why do all these functions return the same value?

Run the classic closure trap: a list of lambdas that all return the same value. See why Python binds late and three clean ways to fix it.

INTERVIEW · vizlearn.in/interview/closures-and-late-binding.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.