*args & **kwargs was about binding a call to parameters. This page is about a different binding: a nested function that reads a name from an enclosing function binds to that name's storage cell, not to whatever value happened to be in it when the nested function was written. That single fact explains the single most-reported Python surprise for anyone coming from Java's captured-effectively-final lambdas — three lambdas built in a loop that all print the same number — and the two idiomatic ways to avoid it.
When a nested function refers to a name from the function that encloses it, CPython does not copy the
value in. It creates a cell — a small box that outlives the enclosing call — and both the enclosing
frame's variable and the nested function's __closure__ point at that same cell. Reading
the name, from either side, always reads whatever is in the cell right now. Pick an experiment and
watch which boxes end up wired to the same cell, and which don't.
A lambda (or any nested def) that mentions a free variable does not "remember"
that variable's value from the line where the lambda was written. It remembers where to look — the
cell — and looks there fresh every time it is called. If the loop that created three such lambdas has already
finished by the time any of them run, every one of them looks at the same cell and finds the same final
value. This is not a bug and not specific to lambda: an ordinary nested def inside
a loop has the exact same behaviour, lambda just makes it fit on one line and therefore shows
up in code review far more often.
The idiomatic fix is the lambda i=i: i pattern above: a parameter's default value is
evaluated exactly once, at def time, and stored directly on the function object — no cell, no
later lookup, no shared state to be surprised by. It looks like it's rebinding i to itself, but
what it is really doing is smuggling the current value in through a different door: parameter defaults.
When the value you want to freeze in is a whole set of arguments rather than one loop variable,
functools.partial(fn, *args, **kwargs) is the same idea, spelled for the general case: it
builds a new callable with some arguments already baked in, evaluated once, right when you call
partial — not a closure over anything that might still change.
nonlocal lets an inner function write through to that cell
instead of shadowing it with a new local. The fix for the loop trap is a default argument
(i=i), evaluated once at def time; functools.partial is the same
freeze-it-now idea for a whole call. fn.__closure__ is the tuple of cells a function actually
closed over, and cell.cell_contents reads what's inside one right now.