Python Closures Explained — How They Work and When to Use Them
Most Python developers hit a wall around the same time: they understand functions, they understand variables, and then they write slightly more complex code and something weird happens — a function 'remembers' a value it seemingly shouldn't have access to anymore. That moment of confusion is actually you bumping into one of Python's most powerful and elegant features: closures. They're not an advanced academic concept — they're built into how Python evaluates and stores functions, and once you get them, a whole class of real-world problems becomes easy to solve.
The problem closures solve is surprisingly common: how do you give a function some persistent, private state without creating a whole class? Before closures, the only clean answer was to write a class with an __init__ method and self.some_value sprinkled everywhere — a lot of ceremony for something conceptually simple. Closures let you attach state to a function directly, keeping that state private and scoped exactly to where it's needed. They're also the engine under the hood of Python decorators, which means understanding closures is the key to understanding one of Python's most widely-used patterns.
By the end of this article you'll know exactly what makes a closure a closure (it's a specific three-part contract Python enforces), you'll be able to write closures that solve real problems like function factories and stateful callbacks, and you'll know the two gotchas that trip up almost every developer the first time they use closures in a loop. Let's build it up from scratch.
What Python Actually Needs to Form a Closure
A closure isn't magic — it's the result of three specific conditions being true at the same time. First, there must be a nested function (a function defined inside another function). Second, the inner function must reference at least one variable from the outer function's scope — not a global variable, not one of its own local variables, but one that belongs to the enclosing function. Third, the outer function must return the inner function. When all three are true, Python doesn't just return a plain function object. It returns a function object bundled together with a snapshot of the variables it referenced from outside. That bundle is the closure.
You can actually inspect this bundle yourself. Every closure in Python has a __closure__ attribute, which is a tuple of 'cell' objects. Each cell holds one of those remembered variables. If a function has no closure, __closure__ is None. This isn't just trivia — it's confirmation that Python is doing real work to preserve that state for you.
The variable that gets captured is called a 'free variable'. It's free because it isn't local to the inner function and isn't global — it floats between those two worlds, owned by the enclosing scope. Understanding this scoping layer (called the Enclosing scope in Python's LEGB rule) is the key to predicting how closures behave.
# Demonstrates the three conditions that form a closure # and how to inspect the captured state Python stores. def make_multiplier(factor): # Outer function defines 'factor' """Returns a function that multiplies any number by 'factor'.""" def multiply(number): # Inner function — condition 1: nested function return number * factor # References 'factor' from outer scope — condition 2 return multiply # Returns the inner function — condition 3 # Creating two separate multiplier functions from the same template double = make_multiplier(2) triple = make_multiplier(3) print(double(10)) # 'factor' remembered as 2 print(triple(10)) # 'factor' remembered as 3 print(double(7)) # Each closure has its OWN independent 'factor' # Inspecting the closure internals Python stores under the hood print(type(double)) # It's a regular function object print(double.__closure__) # Tuple of cell objects holding captured vars print(double.__closure__[0].cell_contents) # The actual value of 'factor' stored inside print(double.__code__.co_freevars) # Name of every free variable this closure captured
30
14
<class 'function'>
(<cell at 0x...>: int object at 0x...>,)
2
('factor',)
Function Factories — The Most Practical Closure Pattern
The multiplier example above reveals the real power of closures: you can use one function definition as a factory that stamps out dozens of customised, independent functions. Each function produced by the factory carries its own copy of the configuration baked into it at creation time. This is far more lightweight than writing a class, and it keeps your code flat and readable.
A function factory is the pattern you reach for when you find yourself copy-pasting the same function body and only changing one constant. Instead of add_tax_uk, add_tax_us, add_tax_eu as three separate functions, you write one make_tax_adder factory and call it three times. Each result is a standalone function you can pass around, store in a list, or hand to another function — it behaves exactly like any other callable.
This pattern shows up constantly in real codebases: generating URL route handlers with different base paths, creating validators configured with different rules, building event handlers wired to different data sources. Once you see it, you'll spot it everywhere. The key mental model is this — the outer function is the mould, the inner function is the cast object, and the closure variables are the material poured into the mould.
# Real-world function factory: generate tax calculators for different regions. # Each calculator remembers its own tax rate — no class needed. def make_tax_calculator(tax_rate_percent): """Factory that returns a price calculator for a specific tax rate.""" rate_as_decimal = tax_rate_percent / 100 # Pre-compute once, captured in closure def calculate_price_with_tax(net_price): # 'rate_as_decimal' is a free variable — remembered from the outer scope tax_amount = net_price * rate_as_decimal total = net_price + tax_amount return round(total, 2) return calculate_price_with_tax # Stamp out three independent calculators from one function definition uk_vat = make_tax_calculator(20) # UK VAT: 20% us_sales_tax = make_tax_calculator(8.5) # Example US state: 8.5% zero_rated = make_tax_calculator(0) # Zero-rated goods base_price = 49.99 print(f"UK price: £{uk_vat(base_price)}") print(f"US price: ${us_sales_tax(base_price)}") print(f"Zero-rated: £{zero_rated(base_price)}") # You can store them in a dict and look them up dynamically — very useful in APIs region_calculators = { "UK": uk_vat, "US": us_sales_tax, "EU": make_tax_calculator(21), # Germany VAT: 21% } region = "EU" print(f"EU price: €{region_calculators[region](base_price)}")
US price: $54.24
Zero-rated: £49.99
EU price: €60.49
Closures With Mutable State — Building a Counter Without a Class
So far the closure variables have been read-only inside the inner function. But what if you need the inner function to update that variable — to keep a running count, for example? This is where Python's nonlocal keyword comes in. Without it, any assignment inside the inner function creates a brand-new local variable and shadows the outer one rather than updating it. Python treats assignment as declaration by default.
nonlocal is your explicit instruction to Python: 'when I assign to this name, go up to the enclosing scope and update the variable there, don't create a new local one.' It's the closure equivalent of the global keyword, but scoped to the enclosing function rather than the module level. You should reach for nonlocal only when you genuinely need mutable state in a closure — if you're using it everywhere, a class is probably the cleaner choice.
The stateful counter pattern is a textbook example, but the real-world version is more interesting: rate limiters, request counters, retry trackers, and progress accumulators all use this exact shape. The closure gives each counter its own private state, completely isolated from any other counter you create from the same factory.
# Simulates an API endpoint call counter with a reset capability. # Uses 'nonlocal' to mutate state stored in the closure. def make_request_counter(endpoint_name): """Returns a counter function tied to a specific API endpoint.""" call_count = 0 # This is the state we want to mutate from inside the inner function def record_call(): nonlocal call_count # Tell Python: update the OUTER 'call_count', don't create a new local one call_count += 1 print(f"[{endpoint_name}] Call #{call_count}") def get_count(): return call_count # Read-only — no 'nonlocal' needed for reads def reset_count(): nonlocal call_count call_count = 0 print(f"[{endpoint_name}] Counter reset.") # Return multiple inner functions as a named tuple for clean access # All three share the SAME 'call_count' cell — they're all part of one closure return record_call, get_count, reset_count # Create independent counters for two different endpoints track_login, get_login_count, reset_login = make_request_counter("/api/login") track_search, get_search_count, reset_search = make_request_counter("/api/search") track_login() track_login() track_search() track_login() print(f"Total login calls: {get_login_count()}") print(f"Total search calls: {get_search_count()}") reset_login() print(f"Login count after reset: {get_login_count()}")
[/api/login] Call #2
[/api/search] Call #1
[/api/login] Call #3
Total login calls: 3
Total search calls: 1
[/api/login] Counter reset.
Login count after reset: 0
The Classic Loop-Closure Gotcha (And Two Ways to Fix It)
Here's the trap that catches almost every developer writing closures inside a loop for the first time. You want to create a list of functions, each capturing a different loop variable. You write what looks like perfectly reasonable code, run it, and every single function in the list returns the same value — the last value the loop variable had. This isn't a bug in Python. It's the correct behaviour once you understand what closures actually capture.
A closure captures a variable by reference, not by value. That means all the inner functions point to the same index variable in the enclosing scope. By the time you call any of those functions, the loop has finished and index has settled at its final value. Every function looks up index right then, at call time, and sees the same thing.
There are two clean fixes. The first is to use a default argument in the inner function (def greet(name=name):), which forces the current value to be captured by value at definition time. The second is to wrap the inner function in another function call that immediately captures the current value in its own closure scope. Both work — the default argument approach is slightly more Pythonic for simple cases.
# Demonstrates the classic loop-closure bug and TWO correct fixes. user_names = ["alice", "bob", "carol"] # ---- THE BUG ------------------------------------------------ buggy_greeters = [] for name in user_names: def greet_buggy(): return f"Hello, {name}!" # Captures 'name' BY REFERENCE — looks it up at call time buggy_greeters.append(greet_buggy) # Loop is done, 'name' is now "carol" for everyone print("Buggy output:") for greeter in buggy_greeters: print(greeter()) # Every function sees the LAST value of 'name' # ---- FIX 1: Default Argument (captures value at definition time) ---- fixed_greeters_v1 = [] for name in user_names: def greet_with_default(captured_name=name): # Default arg evaluated NOW, not at call time return f"Hello, {captured_name}!" fixed_greeters_v1.append(greet_with_default) print("\nFix 1 — Default argument:") for greeter in fixed_greeters_v1: print(greeter()) # ---- FIX 2: Wrapping factory (creates a fresh enclosing scope each iteration) ---- def make_greeter(person_name): # Each call to make_greeter creates a NEW scope def greet(): # 'person_name' is bound fresh in each scope return f"Hello, {person_name}!" return greet fixed_greeters_v2 = [make_greeter(name) for name in user_names] print("\nFix 2 — Factory function:") for greeter in fixed_greeters_v2: print(greeter())
Hello, carol!
Hello, carol!
Hello, carol!
Fix 1 — Default argument:
Hello, alice!
Hello, bob!
Hello, carol!
Fix 2 — Factory function:
Hello, alice!
Hello, bob!
Hello, carol!
| Feature / Aspect | Closure | Class with __init__ |
|---|---|---|
| State storage | Free variables in closure cell | Instance attributes via self |
| Syntax overhead | Low — just nested functions | High — class, __init__, self everywhere |
| Multiple methods sharing state | Possible but awkward (return tuple of funcs) | Natural — all methods share self |
| Introspection / debugging | Requires __closure__ inspection | Standard attribute access on instance |
| Best used when | One primary callable with light state | Multiple methods or complex state |
| Supports inheritance | No | Yes |
| Callable directly | Yes — it's just a function | Only if __call__ is defined |
🎯 Key Takeaways
- A closure is a function bundled with its free variables — Python stores those variables in cell objects accessible via
__closure__, not just in a stack frame that disappears. - Closures capture variables by reference, not by value — this means loop closures all see the final loop value unless you force value capture with a default argument or a factory function.
- Use
nonlocalexplicitly whenever an inner function needs to assign to (not just read) a variable from the enclosing scope — omitting it silently creates a new local variable and causesUnboundLocalError. - Closures are the right tool for lightweight stateful callables and function factories; when state grows complex or you need multiple methods, reach for a class instead.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Forgetting
nonlocalwhen mutating an enclosed variable — Symptom:UnboundLocalError: local variable 'count' referenced before assignmenteven though 'count' clearly exists in the outer function — Fix: Addnonlocal countas the first line of the inner function. Python needs that explicit declaration to know you mean the outer variable, not a new local one. - ✕Mistake 2: Assuming loop closures capture the value at the time of creation — Symptom: Every function in a list created inside a loop returns the same result (the last loop value) when called — Fix: Use a default argument (
def func(val=loop_var):) or a factory function to force value capture at definition time instead of reference capture. - ✕Mistake 3: Overusing closures when a class is the right tool — Symptom: You end up returning 4+ inner functions from one outer function and the code becomes harder to read than a simple class would be — Fix: If your closure needs more than 2-3 inner functions or the state grows complex, refactor to a class. Closures shine for lightweight, single-callable patterns — don't stretch them beyond that.
Interview Questions on This Topic
- QCan you explain what a closure is in Python and describe the three conditions required to form one? Then walk me through what `__closure__` and `__code__.co_freevars` actually contain.
- QWhat will this code print and why? `funcs = []; [funcs.append(lambda: i) for i in range(3)]; print([f() for f in funcs])` — How would you fix it?
- QWhat is the difference between using `global` and `nonlocal` in Python? If I have a three-level nested function, which scope does `nonlocal` refer to, and what happens if the variable isn't found in the immediately enclosing scope?
Frequently Asked Questions
What is a closure in Python and how is it different from a regular nested function?
A regular nested function is just a function defined inside another function. A closure is a nested function that references at least one variable from the enclosing function's scope AND is returned (or otherwise used outside) so that enclosing scope would normally be gone. Python detects this and keeps those referenced variables alive in cell objects attached to the inner function.
When should I use a closure instead of a class in Python?
Use a closure when you need a single callable with a small amount of private configuration or state — function factories and simple counters are ideal. Switch to a class when you need multiple methods, more complex state management, inheritance, or when you want straightforward instance.attribute introspection. The rule of thumb: if you're returning more than two inner functions, a class will be cleaner.
Does Python's garbage collector clean up closure variables if the closure is no longer referenced?
Yes. The free variables are kept alive by reference counting and the garbage collector as long as at least one closure that references them still exists. Once all closures referencing those cell objects are garbage-collected, the cell objects — and their contents — are freed too. There's no special memory leak risk unique to closures beyond ordinary reference cycles.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.