A closure is a nested function that remembers variables from its
enclosing scope even after that outer function has returned. The
remembered names are called free variables — they're neither local
parameters nor globals. Python stores them on the function's __closure__
attribute.
def multiplier(factor):
def multiply(n):
return n * factor # 'factor' is a free variable
return multiply
double = multiplier(2)
double(5) # 10
double.__closure__[0].cell_contents # 2 — captured value
The inner function keeps the binding alive via a cell object, which is
why multiplier can return and double still works. Closures are how
Python functions carry private state without a class.
By default, assigning to a name inside a function creates a new local.
nonlocal tells Python that an assignment should instead rebind a
variable in the nearest enclosing function scope — letting a closure
mutate, not just read, the captured variable.
def counter():
count = 0
def increment():
nonlocal count # rebind outer 'count'
count += 1
return count
return increment
c = counter()
c(); c() # 1, then 2
Without nonlocal, count += 1 would raise UnboundLocalError (it reads
then assigns a local). Use nonlocal for enclosing-function scope and
global for module scope.
Closures capture variables, not values — this is late binding. A function created in a loop looks up the loop variable when it's called, not when it's defined, so every closure sees the variable's final value.
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] — all see final i
# fix: bind the current value via a default argument
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
The default-argument trick works because defaults are evaluated at
definition time, snapshotting i per iteration. A factory function that
takes i as a parameter achieves the same. This is a favorite interview
gotcha.
A function with free variables has a non-None __closure__ — a tuple
of cell objects, each holding one captured binding accessible via
cell_contents. The matching names are listed in
__code__.co_freevars. Functions with no closure have __closure__ is None.
def make(x, y):
def inner():
return x + y
return inner
f = make(3, 4)
f.__code__.co_freevars # ('x', 'y')
[c.cell_contents for c in f.__closure__] # [3, 4]
This is mostly useful for debugging or teaching how closures actually
store state. The cells are shared live, so nonlocal rebinds are visible
through cell_contents.
Both bundle behavior with state. A closure is lighter and ideal when you need a single method and a little hidden state. A class wins when you need multiple methods, inheritance, or explicit, inspectable state. Common closure uses include factories, decorators, and callbacks.
# closure: tiny stateful function
def make_adder(n):
return lambda x: x + n
add10 = make_adder(10)
# class: equivalent but heavier
class Adder:
def __init__(self, n): self.n = n
def __call__(self, x): return x + self.n
Rule of thumb: one behavior + private state → closure; many behaviors or shared interface → class. Decorators are the canonical real-world closure.
The closures all reference the same variable, which holds its final
value after the loop. Capture the current value with a default argument
(bound at definition) or functools.partial.
fns = [lambda: i for i in range(3)]
[f() for f in fns] # [2, 2, 2] -> all see final i
fns = [lambda i=i: i for i in range(3)]
[f() for f in fns] # [0, 1, 2] -> default binds now
Rule of thumb: bind the loop variable as a default argument (x=x) to capture
its value at each iteration.
A closure keeps state encapsulated in the enclosing scope — invisible to and unmodifiable by unrelated code — whereas a global is shared mutable state anyone can clobber. Each closure instance also gets its own independent state.
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
a, b = counter(), counter()
a(); a(); b() # a -> 2, b -> 1 (separate state)
Rule of thumb: use closures to encapsulate private, per-instance state instead of leaking it into globals.
A closure stores a reference to the variable, not a snapshot of its value — so it reads the variable's current value when called, not when defined. This late binding is why loop closures surprise people and why mutating an enclosed variable later affects all closures over it.
x = 10
f = lambda: x
x = 20
f() # 20 -> reads x at call time
Rule of thumb: closures see live variables; bind a value explicitly (default arg) if you need it frozen.
Enclosed variables become cell objects shared between the outer and inner
function. The inner function's __closure__ is a tuple of these cells, and
its code lists them in __code__.co_freevars. Reading/writing the free
variable goes through the cell, which is how state stays shared and live.
def outer():
x = 1
def inner(): return x
return inner
f = outer()
f.__closure__[0].cell_contents # 1
f.__code__.co_freevars # ('x',)
Rule of thumb: free variables are stored in cells; inspect __closure__ to
see what a function captured.
nonlocal rebinds a name in the nearest enclosing function scope;
global rebinds a name at module scope. Without either, an assignment
inside a function creates a new local, shadowing the outer name.
x = "module"
def outer():
x = "enclosing"
def inner():
nonlocal x; x = "changed" # affects outer's x
# global x -> would affect the module-level x
inner()
return x # "changed"
Rule of thumb: nonlocal for enclosing-function state, global for
module-level state; assignment alone always makes a local.
Capture a cache dict in the enclosing scope; the inner function reads and
writes it across calls. This is the manual version of functools.lru_cache
and a classic closure use.
def memoize(fn):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = fn(*args)
return cache[args]
return wrapper
slow = memoize(slow)
Rule of thumb: a closure over a cache dict gives per-function memoization;
reach for lru_cache for the battle-tested version.
A function that returns a customized function, with configuration captured in a closure. It lets you generate specialized functions from parameters without repeating code.
def power_of(exp):
def raise_(base):
return base ** exp
return raise_
square, cube = power_of(2), power_of(3)
square(5), cube(2) # 25, 8
Rule of thumb: use a factory closure to stamp out related functions parameterized by captured values.
Any assignment to a name inside a function makes it local for the whole
function body, so reading it before the assignment — even to do n += 1 —
fails with UnboundLocalError. Declare it nonlocal (or global) to rebind
the outer one instead.
def counter():
n = 0
def inc():
n += 1 # UnboundLocalError: n treated as local
return n
return inc
# fix: add `nonlocal n` at the top of inc
Rule of thumb: to modify (not just read) an enclosing variable, you must
declare it nonlocal.
Yes — as long as the closure exists, the cells holding its free variables
keep those objects referenced and uncollectable. A long-lived closure
capturing a large object (or self) can therefore cause a memory leak if
you forget about it.
def make():
big = load_huge_data()
return lambda: len(big) # `big` stays alive via the closure
f = make() # huge data retained until f is dropped
Rule of thumb: be mindful that closures pin their captured objects in memory for as long as the closure lives.
A closure reads the variable lazily at call time (late binding); a default argument snapshots the value eagerly at definition time (early binding). That distinction is exactly what fixes the loop-closure bug.
x = 1
late = lambda: x # reads x when called
early = lambda x=x: x # froze x = 1 at definition
x = 99
late(), early() # (99, 1)
Rule of thumb: closure = live/late, default arg = frozen/early — choose based on whether you want the current or captured value.
More Functions interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.