Closures & nonlocal
- Create functions that remember variables from their enclosing scope
- Use closures to build small, stateful helpers
- Distinguish closures from classes with
__call__ - Avoid the late-binding gotcha with default arguments
Every decorator, every functools.partial, and every FastAPI dependency-injection callable is a closure under the hood. Understanding late binding versus early binding is why senior engineers write def f(x, _cached=expensive()) and why lambda i: i inside a loop is a classic bug in JavaScript-to-Python converts.
- Late-binding in loops —
[lambda: i for i in range(3)]all return 2; uselambda i=i: ito capture eagerly. - Rebinding an outer variable without
nonlocal— Python creates a new local and the closure appears broken. - Leaking large objects in closure cells — a captured DataFrame keeps memory alive; use
weakrefor explicitdel.
A closure is a function that captures variables from its enclosing scope.
Why nonlocal?
By default, assigning to a name inside a function creates a new local. nonlocal says "no, write to the outer variable."
When closures are useful
- Function factories (
make_counter,make_adder) - Lightweight state without a class
- Decorators (every decorator is a closure)
Gotcha
Classic loop pitfall: [lambda: i for i in range(3)] — all three lambdas see i == 2. Capture explicitly with a default argument: lambda i=i: i.
Try it
- Write
make_adder(n)that returns a function addingnto its argument. - What does
[f() for f in [lambda: i for i in range(3)]]produce?
3 graded exercises
Write the code, click Check. We’ll tell you exactly what to fix.
- Exercise 1
Write a factory
multiplier(n)that returns a function which multiplies its argument byn. Then createdouble = multiplier(2)andtriple = multiplier(3). Printdouble(10)andtriple(10). Expected:20,30. - Exercise 2
Write
make_counter()that returns a function which, when called, increments and returns an internal counter. Test: createc = make_counter(), thenc(); c(); c()— the third call should return3. Usenonlocal. - Exercise 3
Write
accumulator()that returns a function taking a number and returning the running total across all calls. First call:acc(10)→10. Second:acc(5)→15. Third:acc(3)→18. Print those three results.