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 in outer_function‘s scope but is used inside inner_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 if inner_function needs 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 nonlocal only 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 for loop; capture the current value with a default argument (def f(x, i=i):) or with functools.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 a name argument and returning a string like \"Hello, Alice!\" using the captured greeting. 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 alternating True and False values, starting with True. You’ll need nonlocal to 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 nonlocal keyword.
  • Under the hood, CPython shares free variables through cell objects, 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.