Free demoJob-oriented courses at Debugshala, Old Palasia, Indore. Freshers, non-IT and year-gap welcome.Book a free demo +91-8982385539

Python Closures: Nested Functions, nonlocal and Factories

Lesson 46 of 52 · 8 min · Updated 13 Nov 2026

Learn Python closures: nested functions that remember outer variables, __closure__ cells, nonlocal state, function factories and the late-binding loop trap.

Walk into a clothing store in Indore on the first day of the Diwali sale. The staff use price-tagging guns. Before opening, the manager sets one gun to "10% off" and another to "25% off". After that, nobody touches the settings.

A staff member picks up the 25% gun and tags sarees all day. The gun does not need to be told the discount each time; it remembers what the manager set. The manager may have left for a meeting, but the setting stays inside the gun.

That is a closure. The manager setting up the gun is the outer function. The gun is the inner function that gets returned. The remembered "25%" is a captured variable, kept alive even though the outer function has finished.

A closure in Python is a nested function object that retains access to variables from its enclosing function's scope after that enclosing function has returned, so the inner function carries its own remembered data.

The Python variable scope lesson introduced the enclosing scope and nonlocal; this lesson shows what closures are good for. It follows Python generators, another way functions keep state. Examples target Python 3.12+.

What is a closure in Python?

Three conditions make a closure. There is a nested function. The nested function uses a variable from the outer function. And the outer function returns the nested function, so it outlives the outer call.

Python
def make_discount(percent):
    def apply(price):
        return round(price * (1 - percent / 100), 2)
    return apply

sale_10 = make_discount(10)
sale_25 = make_discount(25)

print(sale_10(2400), sale_25(2400))
prices = [999, 1450, 3200]
print([sale_25(p) for p in prices])
print(sale_25.__name__, "remembers percent =", sale_25.__closure__[0].cell_contents)

When make_discount(25) returns, its local variable percent would normally disappear. But apply still needs it, so Python keeps it alive and attaches it to the returned function. Each call to make_discount creates a fresh variable, so sale_10 and sale_25 never interfere.

How do closures remember values?

Python stores each captured variable in a small container called a cell. The outer function and the inner function both point to the same cell. When the outer function finishes, the cell stays, because the inner function still refers to it.

Python closure cells: make_discount(10) and make_discount(25) each return an apply function linked to its own cell holding percent

Every call to the outer function creates a new cell, so each closure carries its own value.

Python
def make_gst_calculator(rate, label):
    def calc(amount):
        return f"{label}: {amount} + {round(amount * rate, 2)} GST"
    return calc

gst18 = make_gst_calculator(0.18, "Standard")
print(gst18(1450))
print("Free variables:", gst18.__code__.co_freevars)
print("Cell values:", [c.cell_contents for c in gst18.__closure__])

def plain(amount):
    return amount
print("A normal function has no closure:", plain.__closure__)

__code__.co_freevars lists the names the function borrows from outside, and __closure__ holds a cell for each one. A normal top-level function has __closure__ set to None. You rarely inspect these in real code, but they prove the remembered values are genuinely attached to the function.

Quick check

Which keyword lets an inner function reassign a variable of its enclosing function?

Changing captured state with nonlocal

Reading a captured variable needs nothing special. Assigning to it does: without nonlocal, Python would treat the name as a new local variable of the inner function. With nonlocal, the closure can update its own private state between calls.

Python
def make_running_total():
    total = 0
    count = 0
    def add(amount):
        nonlocal total, count
        total += amount
        count += 1
        return f"{count} bills, total {total}, average {total / count:.1f}"
    return add

counter_a = make_running_total()
counter_b = make_running_total()
for bill in [1450, 980, 2300]:
    print("A:", counter_a(bill))
print("B:", counter_b(640))

Each counter keeps its own total, hidden from the rest of the program. Nothing outside can accidentally reset it, which is the main attraction compared with a global variable.

Function factories for data work

Closures shine as function factories: functions that build configured functions. In analysis you often need many similar rules (band this column, validate that range), and a factory lets you write the logic once and create each variant in one line.

Python
import pandas as pd

def make_band(edges, labels):
    def band(value):
        for edge, label in zip(edges, labels):
            if value < edge:
                return label
        return labels[-1]
    return band

def make_range_check(low, high):
    return lambda v: low <= v <= high

order_band = make_band([1000, 5000], ["small", "medium", "large"])
valid_qty = make_range_check(1, 50)

df = pd.DataFrame({"amount": [640, 1450, 12300, 2300], "qty": [1, 3, 80, 2]})
df["band"] = df["amount"].map(order_band)
df["qty_ok"] = df["qty"].map(valid_qty)
print(df.to_string(index=False))

The lambda inside make_range_check is a closure too: it captures low and high. Short lambda functions are a natural fit for small factories like this.

The late-binding trap in loops

Closures look up a captured variable's value when they are called, not when they are created. If you build functions inside a loop, they all share the loop variable's single cell and see its final value.

Python
rates = [0.05, 0.12, 0.18]

buggy = [lambda amt: round(amt * r, 2) for r in rates]
print("Buggy:", [f(1000) for f in buggy])          # all use 0.18

fixed = [lambda amt, r=r: round(amt * r, 2) for r in rates]
print("Default arg:", [f(1000) for f in fixed])

def make_tax(r):
    return lambda amt: round(amt * r, 2)
factory = [make_tax(r) for r in rates]
print("Factory:", [f(1000) for f in factory])
Python closure late binding: lambdas created in a loop share one cell for r and all return 180, while the default-argument fix gives each lambda its own value

Shared cell: every lambda reads r at call time. Default argument or factory: each function gets its own value.

The default-argument fix works because default values are evaluated once, at definition time. The factory fix works because each make_tax(r) call creates a new cell. The factory is usually clearer to read.

Quick check

What does this print? fs = [lambda: i for i in range(3)]; print([f() for f in fs])

Closures vs classes vs functools.partial

A closure and a small class can do the same job. The functools.partial helper is a third option when you only need to fix some arguments.

Python
from functools import partial

def apply_discount(price, percent):
    return round(price * (1 - percent / 100), 2)

class Discount:
    def __init__(self, percent):
        self.percent = percent
    def __call__(self, price):
        return apply_discount(price, self.percent)

festive = partial(apply_discount, percent=25)
print(festive(2400), Discount(25)(2400), apply_discount(2400, 25))
Option Best when Downside
Closure One behaviour, a little hidden state State is hard to inspect or change later
Class with __call__ Several methods or visible, changeable state More code
functools.partial You only need to pre-fill arguments No custom logic of its own

Closures are the basis of decorators

A decorator is a closure that receives a function and returns a wrapped version of it. The wrapper remembers the original function in a cell, exactly like apply remembered percent.

Python
def count_calls(func):
    calls = 0
    def wrapper(*args):
        nonlocal calls
        calls += 1
        print(f"{func.__name__} call #{calls}")
        return func(*args)
    return wrapper

def monthly_total(values):
    return sum(values)

monthly_total = count_calls(monthly_total)
print(monthly_total([412, 298]))
print(monthly_total([431]))

The @decorator syntax and functools.wraps are the subject of the next lesson, Python decorators.

Common mistakes with Python closures

  • Forgetting nonlocal when assigning. total += amount without it raises UnboundLocalError.
  • Creating closures in a loop without binding the value. All of them see the last value. Use a default argument or a factory.
  • Calling the inner function inside the outer one. return apply() returns a value; return apply returns the function.
  • Hiding too much state. If you keep adding captured variables, a small class is easier to debug.
  • Capturing a large object by accident. A closure keeps its captured DataFrame alive in memory as long as the function exists.

Key takeaways

  • A closure is an inner function that remembers outer variables after the outer call returns.
  • Captured variables live in cells, visible through __closure__.
  • Each call of the outer function creates fresh cells, so closures do not interfere.
  • Use nonlocal to update captured state.
  • Function factories create configured functions such as discounters and validators.
  • Late binding affects loops; fix it with a default argument or a factory.

Continue with Python decorators, which are built entirely on closures. Writing reusable cleaning rules with factories like make_band is the sort of practical Python taught in Debugshala's Old Palasia data analytics sessions.

Take the free Python Programming quiz10 quick questions with explanations. No sign-up needed, and you can come back to this lesson after.

FAQ

What is a closure in Python with an example?

A closure is a nested function that keeps access to variables from the function that created it. In make_discount(25), the returned apply function remembers percent = 25 and uses it on every call, long after make_discount has finished. Each call to the outer function creates an independent closure.

What is the difference between a nested function and a closure?

Every closure is a nested function, but not every nested function is a closure. A nested function becomes a closure when it uses variables from the enclosing function and survives beyond that function's call, usually by being returned. A helper that is only called inside its parent and uses no outer variables is just a nested function.

Why use closures instead of global variables?

Closures keep state private to one function, so other code cannot read or change it by accident. Each closure also has its own copy of the state, so you can create many independent counters or calculators. Global variables are shared by everything, which makes bugs harder to trace.

What is the late binding problem in Python closures?

Closures read captured variables when called, not when created. Functions created in a loop therefore all see the loop variable's last value. Fix it by binding the current value as a default argument, lambda amt, r=r: ..., or by creating each function through a factory call.

How can I practise closures for Python interviews in Indore?

Closure questions usually test late binding, nonlocal and how decorators work. Predict the output of each example above before running it, then try closure and nested-function questions on Debugshala Test. Debugshala's data analytics course at Old Palasia, Indore, begins with a free demo class, in English and Hindi.

From the team behind this site

Go further with the Data Analytics Course at Debugshala

These tutorials are written by Debugshala, a training and placement institute at Old Palasia, Indore. The classroom course adds live mentoring, real projects and interview preparation.

  • Offline classes at Old Palasia, Indore
  • Free demo class before you pay
  • Non-IT, fresher and year-gap friendly
  • Placement assistance

700+ students placed · 200+ hiring partners · 4.9★ Google rating

Python Generators: yield, Generator Expressions and PipelinesPython Decorators: @ Syntax, functools.wraps and Real Examples