Loading PyCodeIt workspace...
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.
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.
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.
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.
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.
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?
Hop directly into the Python trace map to start coding, grading queries, and logging XP metrics to your workspace profile.