A Python closure is a function that remembers variables from its enclosing scope even after that scope is gone. This post shows how that memory works under the hood: lexical scoping, cell objects, the nonlocal keyword, and why every decorator you will ever write is built on a closure.
“The key to understanding complicated things is knowing what not to look at.”
Peter Norvig, PAIP
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 15 minutes
Here is something that should be impossible. A function finishes. Its local variables die. And yet another function keeps using those exact variables, not copies, and even keeps changing them. That is a Python closure. A function with a memory. A function that carries a little bit of state around without needing a class or a global variable.
Think of a vending machine that you load with snacks once and walk away from. The person who stocked it is long gone, but the machine still hands out exactly the snacks they put inside. The stocking function is finished, yet its contents live on inside the machine. A closure works the same way: the outer function packs some variables into the inner function and walks away, and the inner function keeps serving them up.
Closures are the foundation of decorators, callback functions, and factory patterns. Once you get closures, decorators (the next post) stop feeling like magic, because every decorator is just a closure in a fancy hat. This post walks through the mechanism step by step, then shows the real patterns where closures earn their keep.
Table of Contents
The Mystery: How Does This Work?
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram shows how a closure captures variables through cell objects. When an inner function references a variable from its enclosing scope, Python stores that variable in a cell that both functions share. The inner function carries these “free variables” with it even after the outer function has returned and its local scope is gone. This one mechanism is what makes decorators, callbacks, and factory functions possible, because each of them leans on an inner function that still remembers its enclosing environment.
📄 the_mystery.py: variables that outlive their function
def make_counter():
count = 0 # Local variable, should die when make_counter returns
def increment():
nonlocal count # "I am using count from the enclosing scope"
count += 1
return count
return increment # Return the inner function
# make_counter() has finished, so its local scope is gone
counter = make_counter()
print(counter()) # 1, but wait, where is "count" stored?
print(counter()) # 2, count still exists
print(counter()) # 3, and it is being modified
# Make another independent counter
other = make_counter()
print(other()) # 1, a separate count variable
print(counter()) # 4, the original counter continues
▶ Output
1 2 3 1 4
What happened here: make_counter() returned increment, and then its frame was destroyed. But count survived. Why? Because increment still reaches back to count from the enclosing scope, Python tucks count into a cell object attached to increment. That cell keeps the variable alive long after make_counter’s frame is gone. A function bundled with this remembered environment is exactly what we call a closure. Notice too that counter and other each got their own private count, which is why one reads 4 while the other reads 1.
Step-by-Step Execution Trace
Think of moving out of a rented flat. The flat gets emptied and handed back to the landlord, but the one box you left in a friend’s storage room is still yours to open any time. The function frame is the flat, and the cell object is that box. With that picture in mind, let us trace exactly what Python does, step by step, when you call make_counter():
- Step 1: Python creates a frame for
make_counter()with the local variablecount = 0. - Step 2: Python compiles
increment()and sees that it readscountfrom the enclosing scope, so it markscountas a free variable inincrement. - Step 3: When the
def increment():line runs, Python creates the function object and attaches the cell holdingcounttoincrement.__closure__. Thereturn incrementline then simply hands that function object back, cell and all. - Step 4: The
make_counter()frame is destroyed. But the cell object holdingcountsurvives, becauseincrementstill points at it. - Step 5:
counter()runsincrement(), which reachescountthrough the cell object, bumps it up by one, and returns the new value.
Do not take my word for it, Python lets you peek at this machinery directly. Below we build a small greeter factory, use it to greet two friends named Prathamesh and Vinay, and then inspect the closure attributes Python created along the way.
📄 inspect_closure.py: seeing the closure internals
def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}!"
return greet
hello = make_greeter("Hello")
namaste = make_greeter("Namaste")
# Inspect the closure
print(f"hello is a closure: {hello.__closure__ is not None}")
print(f"Free variables: {hello.__code__.co_freevars}")
print(f"Cell contents: {hello.__closure__[0].cell_contents}")
print(f"\nnamaste cell: {namaste.__closure__[0].cell_contents}")
# Use them
print(f"\n{hello('Prathamesh')}")
print(f"{namaste('Vinay')}")
▶ Output
hello is a closure: True
Free variables: ('greeting',)
Cell contents: Hello
namaste cell: Namaste
Hello, Prathamesh!
Namaste, Vinay!
What happened here: hello.__closure__ holds the cell objects, which are Python’s way of keeping free variables alive. co_freevars lists the variable names that were captured from the enclosing scope, here just greeting. Each call to make_greeter() builds a brand new closure with its own cell objects, which is why hello remembers "Hello" while namaste remembers "Namaste", with no risk of one overwriting the other.
Practical Closures: Factory Functions
A factory function is a function that builds and returns another function, pre-loaded with some setting. Think of a coffee machine where you pick the cup size once, and every cup it pours after that comes out the same size. You configure it once, then reuse it again and again. Here are three factories you would genuinely reach for at work: a multiplier, a logger with a fixed prefix (we will use it to trace a request from a user named Rahul through an Application Programming Interface (API)), and a reusable input validator.
📄 factories.py: closures as function factories
# Factory 1: Multiplier factory
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
to_inr = make_multiplier(83.5) # USD to INR
print(f"double(15) = {double(15)}")
print(f"triple(15) = {triple(15)}")
print(f"$100 = ₹{to_inr(100):,.0f}")
# Factory 2: Logger with prefix
def make_logger(prefix):
def log(message):
from datetime import datetime
timestamp = datetime.now().strftime("%H:%M:%S")
print(f"[{timestamp}] [{prefix}] {message}")
return log
api_log = make_logger("API")
db_log = make_logger("DB")
api_log("Request received from Rahul")
db_log("Query executed in 0.003s")
api_log("Response sent: 200 OK")
# Factory 3: Validator factory
def make_range_validator(min_val, max_val, field_name="value"):
def validate(value):
if not (min_val <= value <= max_val):
raise ValueError(f"{field_name} must be between {min_val} and {max_val}, got {value}")
return value
return validate
validate_age = make_range_validator(0, 34, "Age")
validate_score = make_range_validator(0, 100, "Score")
print(f"\nValid age: {validate_age(28)}")
print(f"Valid score: {validate_score(95)}")
try:
validate_age(45)
except ValueError as e:
print(f"Error: {e}")
▶ Output
double(15) = 30 triple(15) = 45 $100 = ₹8,350 [14:30:15] [API] Request received from Rahul [14:30:15] [DB] Query executed in 0.003s [14:30:15] [API] Response sent: 200 OK Valid age: 28 Valid score: 95 Error: Age must be between 0 and 34, got 45
What happened here: Each factory call captures its own setting and bakes it into the returned function. double remembers factor = 2, triple remembers 3, and to_inr remembers 83.5, all kept in separate cell objects. The logger does the same trick with a fixed prefix, and the validator remembers its range and field name. You configure the behaviour once, hand back a tiny ready-to-use function, and call it as often as you like. The timestamps in your run will differ from these, since they are read from the clock at the moment each log line prints.
The Catch: Closures in Loops
This is the classic Python closure trap, and it catches even seasoned developers. Here is the short version of why it happens: a closure remembers the variable, not the value the variable had at the time. Watch.
📄 loop_trap.py: the most famous closure bug
# THE BUG: All functions print 4 (the final value of i)
functions = []
for i in range(5):
functions.append(lambda: i)
print("Bug (all return last value of i):")
print([f() for f in functions]) # prints [4, 4, 4, 4, 4], not [0, 1, 2, 3, 4]
# WHY: all lambdas share the SAME variable i (by reference, not by value)
# When called, they all read the current value of i, which is 4 after the loop
# FIX 1: Default argument captures the VALUE at creation time
functions = []
for i in range(5):
functions.append(lambda i=i: i) # i=i captures current value
print("\nFix 1 (default argument):")
print([f() for f in functions]) # [0, 1, 2, 3, 4]
# FIX 2: Use a factory function (proper closure)
def make_func(n):
def func():
return n
return func
functions = [make_func(i) for i in range(5)]
print("\nFix 2 (factory function):")
print([f() for f in functions]) # [0, 1, 2, 3, 4]
▶ Output
Bug (all return last value of i): [4, 4, 4, 4, 4] Fix 1 (default argument): [0, 1, 2, 3, 4] Fix 2 (factory function): [0, 1, 2, 3, 4]
What happened here: Closures capture variables by reference, not by value. All five lambdas point to the very same i, the way five sticky notes can all point at one whiteboard. By the time you actually call them, the loop has finished and the whiteboard reads 4, so every note reads 4 too. The fix is to give each iteration its own scope. A default argument (i=i) grabs the current value the instant the lambda is defined, and a factory function does the same by creating a fresh n on every call to make_func.
The nonlocal Keyword
So far the closures have only read their captured variables. The moment you want to change one, you hit a wall, and nonlocal is the keyword that gets you through it. Picture a shared family wallet on the kitchen counter. Anyone can add money to it, but only if everyone agrees they are touching the same wallet and not quietly starting a new one in their pocket. That agreement is what nonlocal spells out to Python.
📄 nonlocal_demo.py: modifying captured variables
def make_accumulator():
total = 0
def add(amount):
nonlocal total # Without this, "total += amount" makes a NEW local variable
total += amount
return total
return add
wallet = make_accumulator()
print(f"Add 1000: ₹{wallet(1000):,}")
print(f"Add 2500: ₹{wallet(2500):,}")
print(f"Add 500: ₹{wallet(500):,}")
# What happens WITHOUT nonlocal:
def broken_accumulator():
total = 0
def add(amount):
# total += amount # UnboundLocalError: cannot access local variable 'total'
# Python sees "total =" and assumes total is a NEW local variable,
# but you are reading it before assigning it, so it blows up
pass
return add
▶ Output
Add 1000: ₹1,000 Add 2500: ₹3,500 Add 500: ₹4,000
What happened here: nonlocal tells Python a simple thing: “this variable belongs to the enclosing scope, so I am modifying that one, not creating a fresh local copy.” Without it, total += amount trips up, because Python treats any assignment inside a function as creating a local variable, then notices you are reading total before it has a value and raises UnboundLocalError. One handy rule of thumb: if you only read a closure variable, you can skip nonlocal. You only need it when you reassign the variable.
From Closures to Decorators: The Connection
Here is the payoff that makes the next post easy. A decorator is nothing more than a Python closure that captures the function you handed it. Think of a phone case: the phone inside is untouched and works exactly as before, but every knock now goes through the case first. The decorator is the case, your function is the phone. Once you see that, the @ syntax stops being scary and starts looking obvious.
📄 closure_to_decorator.py: every decorator is a closure
import time
def timer(func):
"""A decorator, which is just a closure that captures func."""
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # func is a free variable, captured by the closure
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def find_primes(limit):
"""Find all primes up to limit."""
primes = []
for num in range(2, limit):
if all(num % d != 0 for d in range(2, int(num**0.5) + 1)):
primes.append(num)
return primes
result = find_primes(10000)
print(f"Found {len(result)} primes")
print(f"\nfind_primes.__closure__: {find_primes.__closure__}")
print(f"Captured function: {find_primes.__closure__[0].cell_contents.__name__}")
▶ Output
find_primes took 0.0142s Found 1229 primes find_primes.__closure__: (<cell at 0x7f2a1c3d5e40: function object at 0x7f2a1c3a5d00>,) Captured function: find_primes
What happened here: @timer quietly swaps find_primes for wrapper. That wrapper is a closure holding func, the original find_primes, inside a cell object. So when you call find_primes(10000), you are really calling wrapper(10000), which times the call and then uses the captured func to run the real work. Every decorator in Python is built on this same closure trick. The exact timing and the hexadecimal cell address will differ on your machine and from one run to the next, but 1229 primes below 10000 is fixed. Get comfortable with closures, and decorators stop feeling like magic.
Common Misconceptions
Misconception 1: “Closures capture values”
✅ The reality
# Closures capture REFERENCES to variables, not their values. # That is why the loop trap exists: all closures share the same reference. # And that is why nonlocal works: you are modifying the shared variable.
A closure does not photocopy the value at the moment it is created. It keeps a live link to the variable itself. The two behaviours that confuse beginners, the loop trap and the need for nonlocal, are really the same fact seen from two angles: the variable is shared, not copied.
Misconception 2: “You need a class for state”
✅ The reality
# Closures can hold state just like classes, with far less boilerplate. # Counter with a class: 10+ lines (class, __init__, a method) # Counter with a closure: 5 lines (function, nonlocal, inner function) # Use closures for simple state. Use classes when you need many methods.
You do not always need a class to remember something between calls. A Python closure carries state perfectly well, and for a single counter or accumulator it is shorter and easier to read. Reach for a class once you need several related methods, shared inheritance, or a clear public interface. For one small piece of remembered state, a closure wins.
Conclusion
A Python closure is a function that remembers variables from its enclosing scope. The inner function captures the binding, not the value, which is exactly why closures in loops can catch you off guard. Factory functions, stateful callbacks, and rate limiters are all everyday closure patterns, and closures are the quiet mechanism that makes decorators possible.
You have now seen how closures hold on to their scope. In the decorators tutorial, you will combine closures with Python’s @ syntax to build decorators, the pattern that lets you change how a function behaves without touching the function’s own code. And if you want to jump to any other topic, the full Python + AI/ML tutorial series home lists every post in order.
Frequently Asked Questions
What is a closure in Python?
A closure is a function that remembers variables from its enclosing scope even after that scope has finished executing. It’s created when an inner function references a variable from an outer function, and the outer function returns the inner function. The captured variables are stored in cell objects accessible via __closure__.
What is a free variable in Python?
A free variable is a variable used inside a function but defined in an enclosing scope (not local, not global). In a closure, free variables are captured by cell objects so they survive after the enclosing function returns. Check a function’s free variables with func.__code__.co_freevars.
What does nonlocal do in Python?
nonlocal declares that a variable in the inner function refers to one in the enclosing scope. Without it, count += 1 inside a closure creates a new local variable (and fails with UnboundLocalError). nonlocal count tells Python to modify the enclosing scope’s count instead.
Why do closures in loops all return the same value?
Closures capture variables by reference, not by value. In a loop, all closures share the same loop variable. When called after the loop, they all see the final value. Fix with default arguments (lambda i=i: i) or a factory function that creates a new scope per iteration.
What is the relationship between closures and decorators?
Every decorator IS a closure. A decorator takes a function, wraps it in an inner function (the wrapper), and returns the wrapper. The wrapper captures the original function as a free variable, and that is a closure. Understanding closures is a prerequisite to understanding decorators.
Interview Questions on Python Closures
The same ideas as they show up in real interviews, framed as scenarios you can practice out loud.
Q: What does func.__closure__ contain, and at what moment does Python create it?
__closure__ is a tuple of cell objects, one for each free variable listed in func.__code__.co_freevars. Python attaches it the moment the def statement (or lambda) for the inner function executes, not when the outer function returns. Each cell holds a live reference to the captured variable, and cell_contents shows its current value. For a function that captures nothing, __closure__ is None.
Q: You wrapped an API handler in a logging closure, and after deployment memory keeps climbing even though each request finishes cleanly. What do you check first?
Check what the closure captured. A closure keeps every object referenced by its free variables alive, so if the wrapper captured something large, say a full response payload or a cache dictionary, that object can never be garbage collected while the closure exists. Inspect handler.__code__.co_freevars and each handler.__closure__[i].cell_contents to see exactly what is being held. The fix is usually to capture only the small pieces you need, for example a request id instead of the whole request object.
Q: Your teammate Anvi writes make_account() that returns two inner functions, deposit and balance, both using the same total variable. She asks whether balance will see money added through deposit. What do you tell her?
Yes. Both inner functions are defined in the same enclosing scope, so they share the very same cell object for total. When deposit modifies it (with nonlocal), balance reads the updated value immediately because it looks into that same shared cell. This shared cell pattern is a common lightweight alternative to writing a class with two methods.
Q: nonlocal and global look similar. What is the difference, and when does nonlocal fail with a SyntaxError?
nonlocal binds to the nearest enclosing function scope and deliberately skips the global scope, while global jumps straight to module level. If no enclosing function actually defines the variable, nonlocal is rejected at compile time with SyntaxError: no binding for nonlocal 'x' found. So nonlocal only makes sense inside nested functions; at module level or in a non-nested function you would use global, or better, restructure the code to avoid shared mutable state.
Q: When would you pick a closure over a class for holding state, and when the other way around?
For one piece of state driving one behaviour, a counter, an accumulator, a pre-configured validator, a closure is shorter and keeps the state genuinely private, since there is no attribute for outside code to poke. Reach for a class once you need several related methods, inheritance, or easy inspection of the state. A rough rule that works in interviews and in code review: one verb, use a closure; a noun with multiple verbs, use a class.
Q: Your colleague Anvay decorates a function with a timing wrapper, and now help() on it shows the wrapper’s name and docstring instead of the original. Why does this happen, and what is the fix?
The decorator replaced his function with the closure wrapper, so __name__, __doc__, and friends now belong to the wrapper, not the original. The fix is to put @functools.wraps(func) on the wrapper, which copies the original function’s metadata across and stores a reference to it in __wrapped__. This is standard practice for every decorator you write, and the decorators post covers it in depth.
Try It Yourself
Write a make_averager() closure that keeps a running average. Each call adds a new number and returns the updated average: avg = make_averager(), avg(10) → 10.0, avg(20) → 15.0, avg(30) → 20.0. Store the running total and count as closure variables.
Reference: the complete, always-current details live in the official Python documentation.
Related Posts
Previous: Python: Advanced Comprehensions, Nested, Generator Expressions, Performance
Next: Python: Decorators Deep Dive, Writing & Real-World Patterns
Series Home: Python + AI/ML Tutorial Series

No comment