backend / python core / tricky questions / 02_late_binding_closures.md

Late-binding closures

2 min read source

Late-binding closures

What is a closure

A closure is a nested function that captures and remembers variables from its enclosing scope, even after that scope has finished executing. The inner function keeps the outer function’s locals alive.

def multiplier(n):
    def multiply(x):      # inner function reads `n` from the enclosing scope
        return x * n      # `n` is a *free variable* — not a param, not a global
    return multiply       # returned with `n` still bound

times3 = multiplier(3)
times3(10)                # 30 — `n=3` survived after multiplier() returned

Three ingredients: a nested function, a free variable it reads from the enclosing scope, and the outer function returning the inner one. Python stores each captured variable in a cell, visible via __closure__:

times3.__closure__[0].cell_contents   # 3

Closures are how decorators, factories, and callbacks carry state without a class. The catch is how they capture — by variable, not by value — which is the gotcha below.

The gotcha

Closures capture variables, not values. The lookup happens when the closure is called, not when it’s created.

Minimal repro

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])   # [2, 2, 2]   ← all capture the same `i`

The classic loop-variable trap. Same with for i in range(3): funcs.append(lambda: i).

Why it happens

Each lambda has a closure over the enclosing scope’s name i. By the time the lambdas run, the loop has finished and i is bound to the last value, 2. Python doesn’t snapshot i at lambda creation.

How to avoid

Pin via default argument (default args ARE evaluated eagerly, see 01_mutable_default_arguments.md):

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])   # [0, 1, 2]

Or use functools.partial:

from functools import partial
funcs = [partial(lambda x: x, i) for i in range(3)]

Or wrap in an immediate function:

def make(i):
    return lambda: i
funcs = [make(i) for i in range(3)]

Interview angle

Almost universal. Often paired with event handlers — for btn in buttons: btn.on_click(lambda: handle(btn)) is broken in the same way.