Python Closures
A closure is a function that remembers the variables from the scope in which it was defined, even after that outer scope has finished executing. Closures let you build functions with their own private, persistent state without writing a class or reaching for a global variable. They quietly power a lot of everyday Python: decorators, callback factories, memoization helpers, and event handlers all rely on closures behind the scenes. Once you understand exactly what a closure captures — and when it captures it — you will avoid one of the most common and confusing bugs new (and experienced) Python developers run into.
Overview / How Closures Work
In Python, functions can be nested inside other functions. Python resolves names using the LEGB rule — Local, Enclosing, Global, Built-in — meaning that code inside a nested function can read variables defined in the function that encloses it, even if that variable isn’t local to the inner function. A variable used inside a nested function but defined in an enclosing function is called a free variable. When the enclosing function returns the nested function as a value, that returned function keeps a live link to the free variables it needs. This bundle of \”function plus captured environment\” is the closure.
The Three Conditions for a Closure
Technically, Python only creates a true closure when three things are true at once:
- There is a nested function (a function defined inside another function).
- The nested function references a variable defined in the enclosing function’s scope (a free variable), not in its own local scope or the global scope.
- The enclosing function returns the nested function (rather than just calling it internally).
If all three hold, the returned inner function carries its enclosing variables with it wherever it goes — long after the outer function’s normal stack frame would otherwise have been discarded. Each separate call to the outer function creates a brand-new, independent environment, so two closures produced by two different calls never share state with each other.
Syntax
There is no special keyword to \”create\” a closure — it emerges automatically from ordinary nested function definitions. The general shape looks like this:
def outer_function(param):\n free_variable = param\n\n def inner_function():\n nonlocal free_variable\n free_variable += 1\n return free_variable\n\n return inner_function\n
outer_function— the enclosing function; its local variables become available to any function nested inside it.free_variable— a variable that lives inouter_function‘s scope but is used insideinner_function.inner_function— the nested function; Python detects it uses a name it doesn’t define locally and looks outward to find it.nonlocal free_variable— required only ifinner_functionneeds to reassign (write to) the free variable. Reading it needs no special keyword.return inner_function— returning the function object (not calling it) is what lets the closure escape and be used later.
Examples
Example 1: A Counter with Private State
def make_counter():\n count = 0\n\n def counter():\n nonlocal count\n count += 1\n return count\n\n return counter\n\n\nc1 = make_counter()\nc2 = make_counter()\nprint(c1())\nprint(c1())\nprint(c2())\nprint(c1())\n
Output:
1\n2\n1\n3\n
Each call to make_counter() creates a fresh count variable and a fresh counter function bound to it. c1 and c2 are independent closures: incrementing c1 never touches c2‘s state, exactly as if each closure owned a private instance variable. Note the nonlocal keyword — without it, the line count += 1 would try to create a brand-new local variable named count inside counter, and Python would raise an error because you can’t increment a variable that doesn’t exist yet.
Example 2: A Function Factory
def make_multiplier(factor):\n def multiplier(x):\n return x * factor\n return multiplier\n\n\ndouble = make_multiplier(2)\ntriple = make_multiplier(3)\nprint(double(5))\nprint(triple(5))\nprint(double(10))\n
Output:
10\n15\n20\n
Here factor is only ever read, never reassigned inside multiplier, so no nonlocal is needed. make_multiplier acts like a factory: each call bakes a different factor into a specialized function. This pattern is how many real-world APIs build configurable callbacks — think of validators, formatters, or event handlers that are all \”the same function\” but pre-loaded with different settings.
Example 3: Accumulating State Across Calls
def make_running_average():\n total = 0\n count = 0\n\n def add(value):\n nonlocal total, count\n total += value\n count += 1\n return total / count\n\n return add\n\n\navg = make_running_average()\nprint(avg(10))\nprint(avg(20))\nprint(avg(30))\n
Output:
10.0\n15.0\n20.0\n
This closure tracks two pieces of state, total and count, across repeated calls — a single nonlocal statement can name multiple variables separated by commas. Division with / always produces a float in Python 3, which is why the output is 10.0 rather than 10. This kind of stateful accumulator is a common lightweight alternative to writing a whole class just to track a running total.
Under the Hood: Cells and Free Variables
CPython implements closures using objects called cells. When the compiler notices that a nested function references a name from an enclosing function, it doesn’t store that variable as an ordinary local in the outer function’s stack frame (which would be destroyed when the outer function returns). Instead, it stores it in a cell object — a small box that holds a reference to the value. Both the outer and inner function share a reference to the same cell, so when the inner function is called later, it still has a live path to that value, and if it uses nonlocal to mutate it, the change is visible through the cell to anyone else sharing it too.
You can inspect this machinery directly. Every function object that closes over free variables has a __closure__ attribute — a tuple of cell objects, one per free variable — and each cell exposes its value through cell_contents.
Inspecting a Closure Directly
def outer():\n x = \"captured\"\n\n def inner():\n return x\n\n return inner\n\n\nfn = outer()\nprint(fn())\nprint(type(fn.__closure__[0]))\nprint(fn.__closure__[0].cell_contents)\n
Output:
captured\n\ncaptured\n
The names of a function’s free variables are also available ahead of time, without even calling it, via fn.__code__.co_freevars. This is exactly the mechanism decorators rely on: a decorator is usually a function that defines a nested wrapper function which closes over the original function being decorated, keeping a live reference to it across every future call.
Common Mistakes
Mistake 1: Late Binding in Loops
The single most common closure bug is assuming a loop variable is captured by its value at the time the function was created. It isn’t — Python closures capture variables by reference, and the variable is looked up only when the inner function actually runs, not when it was defined.
def make_multipliers_wrong():\n funcs = []\n for i in range(3):\n def f(x):\n return x * i\n funcs.append(f)\n return funcs\n\n\nfuncs = make_multipliers_wrong()\nprint([f(10) for f in funcs])\n
Output:
[20, 20, 20]\n
All three functions share the exact same i cell, and by the time any of them are actually called, the loop has finished and i is stuck at its final value, 2. Every function in the list multiplies by 2, not by 0, 1, and 2 as you might expect. The fix is to force each function to capture the current value immediately, typically with a default argument, which is evaluated once at function-definition time rather than at call time:
def make_multipliers_correct():\n funcs = []\n for i in range(3):\n def f(x, i=i):\n return x * i\n funcs.append(f)\n return funcs\n\n\nfuncs = make_multipliers_correct()\nprint([f(10) for f in funcs])\n
Output:
[0, 10, 20]\n
Here i=i copies the current loop value into a fresh parameter default for each function, so each closure gets its own independent value instead of sharing one mutable cell.
Mistake 2: Forgetting nonlocal When Writing to an Enclosing Variable
Reading a free variable needs no special syntax, but the moment you try to assign to a name inside a nested function, Python treats that name as local to the nested function by default — even on lines before the assignment. For example, writing count += 1 inside a nested function without first declaring nonlocal count raises an error, because Python has already decided count is a local variable of that inner function (since it’s assigned to somewhere in its body), and a local variable can’t be read before it has a value. The rule to remember: any name assigned inside a nested function is local unless you explicitly mark it with nonlocal (to reach an enclosing function scope) or global (to reach module scope).
Best Practices
- Use
nonlocalonly when the inner function truly needs to reassign an enclosing variable; if it only reads the value, no keyword is needed. - Prefer closures over classes for small pieces of state — one or two variables and one behavior — and switch to a class once you need multiple methods sharing state or a clearer public interface.
- Watch for the loop late-binding trap any time you build functions inside a
forloop; capture the current value with a default argument (def f(x, i=i):) or withfunctools.partial. - Keep closures small and focused — if a closure starts tracking many independent pieces of mutable state, that’s usually a sign a class would be clearer.
- Remember that closures capture variables, not values — mutable objects (lists, dicts) captured by reference will reflect later mutations even without
nonlocal, since you’re mutating the object, not rebinding the name. - Use closures deliberately for configuration/factory patterns (like
make_multiplier) instead of duplicating near-identical functions with hardcoded constants.
Choosing Between State Patterns
| Pattern | Good for | Downside |
|---|---|---|
| Closure | One or two pieces of private state, a single behavior | Gets awkward with many methods or attributes |
| Global variable | Truly application-wide, single-instance state | Shared mutable state, hard to test or isolate |
| Class instance | Multiple related methods, richer state, inheritance | More boilerplate for very small cases |
Practice Exercises
- Write a function
make_greeter(greeting)that returns a closure taking anameargument and returning a string like\"Hello, Alice!\"using the capturedgreeting. Test it with two different greetings and confirm they don’t interfere with each other. - Write a function
make_toggle()that returns a closure with no arguments; each call should return alternatingTrueandFalsevalues, starting withTrue. You’ll neednonlocalto flip a boolean stored in the enclosing scope. - Fix this loop-based closure bug yourself before checking the lesson’s solution pattern: build a list of three functions in a loop, one per string in
[\"a\", \"b\", \"c\"], where each function returns its own letter repeated 3 times (e.g.\"aaa\",\"bbb\",\"ccc\") when called — without using the default-argument trick, using a helper function that returns the closure instead.
Summary
- A closure is a nested function that remembers variables from its enclosing scope even after that scope has returned.
- Three conditions create a closure: a nested function, a free variable from the enclosing scope, and the enclosing function returning the nested function.
- Reading a free variable needs no special syntax; reassigning one requires the
nonlocalkeyword. - Under the hood, CPython shares free variables through
cellobjects, visible via a function’s__closure__attribute. - Closures capture variables by reference and look them up at call time, which causes the classic late-binding bug in loops — fix it by capturing the current value with a default argument.
- Closures are a lightweight alternative to classes for small amounts of private, persistent state, and they underpin decorators and function factories throughout Python.
