Functions
Closures
Functions that capture and remember their enclosing scope variables
Interview: Advanced concept frequently tested — basis for decorators, callbacks, and stateful functions
A closure is a function that remembers the variables from its enclosing scope even after the enclosing function has finished executing. Closures are a powerful alternative to classes for maintaining state, and they form the foundation of decorators in Python.
Three Conditions for a Closure
- Nested function: There must be a function defined inside another function
- Captured variable: The inner function must reference a variable from the enclosing scope
- Returned function: The enclosing function must return the inner function
How Closures Work Internally
- Cell objects: Python stores captured variables in "cell" objects — accessible via
func.__closure__ - Snapshot of scope: The closure captures the variable's binding, not its value at creation time
- Lifetime: The captured variables persist as long as the closure function exists
- Memory: Closures keep a reference to the enclosing scope — can prevent garbage collection if overused
Closures vs Classes
Closures can replace simple classes that only have one method and store state. make_counter() returning a closure is equivalent to a Counter class with an __init__ and __call__ method. Closures are more lightweight and concise for simple stateful functions.
nonlocal and Closures
- Read-only capture: Inner functions can READ enclosing variables without
nonlocal - Modifying capture: To MODIFY an enclosing variable, you must declare
nonlocalin the inner function - Without nonlocal: Assigning to a variable creates a new local variable — it doesn't modify the enclosing one
Practical Applications
- Decorators: Every decorator is a closure — it captures the wrapped function
- Factory functions: Create customized functions with captured configuration
- Stateful callbacks: Maintain state between callback invocations without global variables
- Memoization: Cache computed results in the enclosing scope
- Partial application: Bind some arguments and return a function expecting the rest
Closure Late Binding
Closures capture variable NAMES, not values. This causes the classic loop bug: [lambda: x for x in range(3)] creates 3 functions that all return 2 (the final value of x). Fix by using default arguments: [lambda x=x: x for x in range(3)].
Use Cases
Implementing decorators that maintain state (call counts, caches, rate limiters)
Factory functions that create customized functions with captured configuration
Replacing simple classes with stateful functions (counters, accumulators)
Partial application — binding some arguments and returning a function for the rest
Memoization caches that persist across function calls
Common Mistakes
Late binding in loops — closures capture variable names, not values; use default args to fix
Forgetting nonlocal when modifying enclosing variables — creates a new local instead
Not understanding that closures capture bindings, not snapshots — the variable can change
Memory leaks — closures keep references to the entire enclosing scope, preventing garbage collection
Using closures when a simple class would be clearer — prefer readability for complex state