Python Closures Explained: How Free Variables Work in Interview Questions
Understand closure mechanics, free variable binding, and the cell object model that underpins Python's scoping rules - with worked interview examples.
Ameer Abdullah
Data Science Graduate · AI/ML & Data Science
A closure is a function that retains access to variables from its enclosing scope even after that scope has finished executing. When Python creates a closure, it stores references to the free variables (variables from the enclosing scope that the inner function uses) in a set of 'cell' objects attached to the inner function.
What Makes a Closure?
def make_multiplier(factor):
def multiply(x):
return x * factor # 'factor' is a free variable
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15In this example, multiply is a closure. After make_multiplier(2) returns, the local scope of that call is gone - but the inner multiply function still holds a reference to 'factor' through a cell object. The cell object keeps 'factor' alive as long as the closure exists. double.__closure__[0].cell_contents returns 2; triple.__closure__[0].cell_contents returns 3.
Late Binding: The Classic Gotcha
fns = []
for i in range(3):
def f():
return i
fns.append(f)
print(fns[0]()) # 2, not 0!
print(fns[1]()) # 2
print(fns[2]()) # 2All three functions close over the same cell object for 'i'. When any of them is called, they look up the current value of 'i' in the cell - which, after the loop finishes, is 2. This is 'late binding': the value is looked up at call time, not at definition time. It is one of the most common Python interview traps.
The Fix: Default Argument Capture
fns = []
for i in range(3):
def f(i=i): # default arg evaluated NOW
return i
fns.append(f)
print(fns[0]()) # 0
print(fns[1]()) # 1
print(fns[2]()) # 2Using a default argument (i=i) forces Python to evaluate 'i' at function definition time and bind it to the default parameter value. Each function now has a different default for its 'i' parameter, so calling them without arguments returns 0, 1, and 2 respectively. This is the canonical Python fix for late-binding closure loops.
The nonlocal Keyword
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3Without 'nonlocal count', the line 'count += 1' would raise an UnboundLocalError because Python would treat 'count' as a new local variable (since it appears on the left side of an assignment), but it has no value yet. The 'nonlocal' keyword tells Python to look for 'count' in the enclosing scope and modify it there, enabling stateful closures without using classes.
Quick Concept Quiz
In a closure created inside a for loop (e.g., def f(): return i), what value does f() return when called after the loop completes?
Ready to test your execution tracing skills?
Hop directly into the Python trace map to start coding, grading queries, and logging XP metrics to your workspace profile.