Closures & LEGB in Practice
A closure is a function that captures variables from its enclosing scope even after the outer function has returned.
A closure is like a backpack that keeps a LIVE walkie-talkie to the outer function's variables — not a photo of them. The variable can change, and the backpack always hears the latest value.
Closures capture variables by reference, not by value. The enclosed variables live in a 'cell' object shared between the closure and the enclosing scope. This is how factories and decorators maintain state across calls. nonlocal allows a closure to rebind (not just read) an enclosing variable.
Each closure has a __closure__ attribute — a tuple of cell objects. cell.cell_contents gives you the captured value. Because closures capture by reference, the classic loop-closure bug occurs: closures created in a loop all share the same variable (they capture the variable, not the value at that moment). Fix: default argument (lambda i=i: ...) or functools.partial. This is also why you need nonlocal to increment a counter in a closure — without it, assignment makes the variable local.
A closure captures variables from its enclosing scope by reference — the cell object holds a live reference, not a snapshot. This lets inner functions maintain state after the outer function returns. The key gotcha is loop closures: all closures created in a loop share the same loop variable. Fix with default arguments: lambda i=i: i. Inspect closures with func.__closure__ and cell.cell_contents.
Classic loop closure bug: funcs = [lambda: i for i in range(3)] — all three lambdas return 2 (the final value of i), not 0, 1, 2. They all capture the same i cell, not a copy of each iteration's value.