Home Python Python Closures Explained — How They Work and When to Use Them

Python Closures Explained — How They Work and When to Use Them

In Plain English 🔥
Imagine you work at a coffee shop and your manager gives you a secret discount code before you go out to serve customers. Even after you've left the back office, you still remember that code and can use it whenever you need it. A Python closure is exactly that — a function that 'carries' a piece of its birthplace with it, even after that birthplace no longer exists. The inner function remembers the variables from the outer function that created it, like a note tucked into its pocket.
⚡ Quick Answer
Imagine you work at a coffee shop and your manager gives you a secret discount code before you go out to serve customers. Even after you've left the back office, you still remember that code and can use it whenever you need it. A Python closure is exactly that — a function that 'carries' a piece of its birthplace with it, even after that birthplace no longer exists. The inner function remembers the variables from the outer function that created it, like a note tucked into its pocket.

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.

closure_anatomy.py · PYTHON
12345678910111213141516171819202122232425
# 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
▶ Output
20
30
14
<class 'function'>
(<cell at 0x...>: int object at 0x...>,)
2
('factor',)
🔥
LEGB Rule Quick Reminder:Python looks up variable names in this exact order: Local → Enclosing → Global → Built-in. The 'Enclosing' layer is closures' home. If you ever wonder why Python finds a variable you didn't define locally, that's the E in LEGB doing its job.

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.

tax_calculator_factory.py · PYTHON
12345678910111213141516171819202122232425262728293031323334353637
# 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)}")
▶ Output
UK price: £59.99
US price: $54.24
Zero-rated: £49.99
EU price: €60.49
⚠️
Pro Tip:Pre-compute values inside the outer function (like `rate_as_decimal` above) rather than in the inner function. The outer function runs once at creation time; the inner function runs every time it's called. Put the expensive work where it happens least.

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.

api_request_counter.py · PYTHON
123456789101112131415161718192021222324252627282930313233343536373839
# 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()}")
▶ Output
[/api/login] Call #1
[/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
⚠️
Watch Out:If you forget `nonlocal` and write `call_count += 1` directly, Python raises `UnboundLocalError: local variable 'call_count' referenced before assignment`. Python sees the assignment and marks `call_count` as local to the inner function — then can't find a value for it before the `+=`. The fix is always just one word: `nonlocal`.

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.

loop_closure_fix.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738
# 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())
▶ Output
Buggy output:
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!
🔥
Interview Gold:This loop-closure bug is one of the most commonly asked Python interview questions. Interviewers don't just want you to say 'use a default argument' — they want you to explain WHY it works: default argument values are evaluated at function definition time, not call time. That explanation is what separates candidates who've memorised the fix from those who truly understand closures.
Feature / AspectClosureClass with __init__
State storageFree variables in closure cellInstance attributes via self
Syntax overheadLow — just nested functionsHigh — class, __init__, self everywhere
Multiple methods sharing statePossible but awkward (return tuple of funcs)Natural — all methods share self
Introspection / debuggingRequires __closure__ inspectionStandard attribute access on instance
Best used whenOne primary callable with light stateMultiple methods or complex state
Supports inheritanceNoYes
Callable directlyYes — it's just a functionOnly 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 nonlocal explicitly 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 causes UnboundLocalError.
  • 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 nonlocal when mutating an enclosed variable — Symptom: UnboundLocalError: local variable 'count' referenced before assignment even though 'count' clearly exists in the outer function — Fix: Add nonlocal count as 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.

🔥
TheCodeForge Editorial Team Verified Author

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.

← PreviousGenerators in PythonNext →Recursion in Python
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged