🐍 Python Course · Stage 1 · Lesson 12 of 89 · Closures
Course Overview →

🟩 Start with the Beginner tab. Read it fully before moving on.

← Lambda Functions Decorators →
thecodex.expert · The Codex Family of Knowledge
Python

Closures in Python

A closure is a function that remembers and accesses variables from its enclosing scope even after that scope has finished executing — the mechanism behind decorators, callbacks, and factory functions.

Python 3.13 docs.python.org Last verified:
Canonical Definition

A closure in Python is a nested function that captures free variables from its enclosing scope. The Python Language Reference defines a closure as a function object that refers to variables from its enclosing scope — these captured variables are stored in the function object's __closure__ attribute as cell objects.

🟩 Beginner

Functions that remember their birthplace

What you'll learn: What a closure is, how to create one, and why nonlocal is needed to reassign a captured variable. Closures are the foundation of decorators, callbacks, and factory functions — understanding them makes those patterns obvious.

How to read this tab: For each code example, trace the variable: where is it defined? Which function captures it? Can it outlive the outer function? Tracing ownership is the key skill for closures.

⏱ 30 min📄 2 sections🔶 Prerequisite: Functions & Scope
The idea

A closure is a function that "remembers" values from where it was created — even after the outer function has returned. Think of it as a function with a backpack: the backpack contains the variables it captured from its birthplace, and it carries them wherever it goes.

A closure is a function + its captured environment. When the inner function is returned, Python doesn't just return the code — it wraps the code together with the variables it needs from the outer scope. Those variables live in cell objects on the heap, not on the stack, so they survive after the outer function returns.

What is a closure?

A closure forms when a nested function (a function defined inside another function) references a variable from the enclosing function's scope. Python keeps that variable alive so the nested function can access it later — even after the outer function has returned and would normally have been cleaned up.

Pythonclosure_basic.py
def make_greeting(greeting):
    """Outer function — defines the greeting."""
    def greet(name):
        """Inner function — a closure that captures 'greeting'."""
        return f"{greeting}, {name}!"
    return greet   # return the inner function, not its result

# make_greeting has finished — but the captured variable lives on
say_hello = make_greeting("Hello")
say_namaste = make_greeting("Namaste")

print(say_hello("Priya"))     # Hello, Priya!
print(say_namaste("Arjun"))   # Namaste, Arjun!
print(say_hello("Rohit"))     # Hello, Rohit!
# 'greeting' is remembered by each closure independently
💡

The nonlocal keyword signals to Python: "this variable lives in the enclosing scope." Without nonlocal, Python sees count += 1 and creates a local variable named count — which causes UnboundLocalError because you're reading it before assigning. nonlocal tells Python to look for count in the enclosing scope instead.

Three conditions for a closure

The Python Language Reference specifies that a closure forms when three things are true: there is a nested function; the nested function refers to a variable defined in the enclosing scope; and the enclosing function returns the nested function (or passes it somewhere else).

Pythonclosure_counter.py
# A counter factory — each call creates an independent counter
def make_counter(start=0):
    count = start  # captured variable
    def counter():
        nonlocal count  # needed to REASSIGN (not just read) the captured var
        count += 1
        return count
    return counter

a = make_counter()
b = make_counter(10)  # independent counter starting at 10

print(a())   # 1
print(a())   # 2
print(b())   # 11
print(a())   # 3  — a and b are independent
print(b())   # 12
nonlocal is required to reassign, not just read

Reading a captured variable works without nonlocal. But if you want to reassign it (count += 1 is count = count + 1), Python treats it as a local variable and raises UnboundLocalError. Add nonlocal count to tell Python it lives in the enclosing scope.

✅ Beginner tab complete

  • I can write a factory function that returns a customised inner function
  • I know the three conditions for a closure: nested function, captured variable, returned function
  • I know why nonlocal is required to REASSIGN (not just read) a captured variable
  • I can explain the classic loop closure bug and how to fix it

Continue to Decorators → — which are built on closures

🔵 Intermediate

Inspecting closures and real-world patterns

What you'll learn: How to inspect a closure's captured variables via __closure__ and __code__.co_freevars. Real-world closure patterns: memoisation, event handlers, partial application.

How to read this tab: Try inspecting __closure__[0].cell_contents on a function you've written. Seeing the captured value in the cell object makes the mechanism concrete.

⏱ 25 min📄 2 sections🔶 Prerequisite: Beginner tab
📎

cell_contents is the actual captured value. func.__closure__ is a tuple of cell objects — one per captured variable. cell.cell_contents gives you the current value. If the outer function modifies the variable after creating the closure, cell_contents reflects the updated value — because they share the same cell object.

The __closure__ attribute and cell objects

Python stores captured variables in cell objects — a special wrapper that allows the inner and outer functions to share the same variable. The closure function's __closure__ attribute holds a tuple of these cells. You can inspect them to see what was captured.

Pythonclosure_inspect.py
def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

# Inspect the closure
print(double.__closure__)            # (<cell at 0x...>,)
print(double.__closure__[0].cell_contents)  # 2
print(triple.__closure__[0].cell_contents)  # 3

# __code__.co_freevars names the captured variables
print(double.__code__.co_freevars)   # ('factor',)
💡

Every decorator is a closure. When you write @timer, the wrapper function captures func from make_timer's scope. Every time you call the decorated function, the wrapper uses the captured func reference. This is why decorators work — they're closures with a specific structure.

Real-world uses of closures

Closures appear throughout Python — often without being named. Every decorator is built on closures. Every callback that remembers context is a closure. Every factory function that produces customised functions is a closure.

Pythonclosure_uses.py
# 1. Memoisation using a closure
def memoised(func):
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper   # wrapper closes over 'cache'

@memoised
def fib(n):
    if n < 2: return n
    return fib(n-1) + fib(n-2)

print(fib(35))  # Fast — results are cached in the closure's 'cache'

# 2. Event handlers — remembering which button was clicked
def make_button_handler(button_id):
    def handler(event):
        print(f"Button {button_id} clicked")
    return handler  # handler closes over button_id

handlers = [make_button_handler(i) for i in range(5)]

# 3. Partial application — fixing some arguments
def power(base, exponent):
    return base ** exponent

square = lambda x: power(x, 2)   # closure via lambda
cube   = lambda x: power(x, 3)

# Better: use functools.partial (same concept, more explicit)
from functools import partial
square = partial(power, exponent=2)
Commonly confused
The classic loop closure bug. [lambda: i for i in range(5)] — all five lambdas return 4, not 0,1,2,3,4. They all close over the same i variable, which ends at 4 after the loop. Fix: [lambda i=i: i for i in range(5)] — bind the current value as a default argument, creating a snapshot instead of a reference.
nonlocal vs global. nonlocal reaches to the immediately enclosing function scope. global reaches to the module level. Never use global for closures — you want the enclosing scope, not the module scope.

✅ Intermediate tab complete

  • I can use __closure__[0].cell_contents to inspect a captured variable
  • I can explain what __code__.co_freevars contains
  • I can write a memoisation closure using a dict captured from the enclosing scope

Continue to Decorators →

🔴 Expert

Cell objects and CPython bytecode

What you'll learn: How CPython implements closures using heap-allocated cell objects. The MAKE_CELL, STORE_DEREF, and LOAD_DEREF bytecode instructions that distinguish closure variables from regular locals.

How to read this tab: Use dis.dis() on a function that creates a closure and compare with dis.dis() on the inner function. The difference in opcodes reveals the mechanism.

⏱ 20 min📄 1 section🔶 Prerequisite: After Stage 2

How CPython implements cell objects

When CPython's compiler sees a variable referenced in an inner function and assigned in an outer function, it marks that variable as a cell variable in the outer function and a free variable in the inner function. At runtime, the outer function allocates a cell object on the heap (not the stack) and stores the variable's value inside it. Both the outer and inner functions access the variable through this shared cell — which is why the inner function continues to see changes to the variable even after calling nonlocal.

Pythonclosure_bytecode.py
import dis

def outer():
    x = 10
    def inner():
        return x
    return inner

# outer's bytecode uses STORE_DEREF (not STORE_FAST) for x
# inner's bytecode uses LOAD_DEREF (not LOAD_FAST) for x
dis.dis(outer)
# MAKE_CELL x    ← allocates the cell object

f = outer()
dis.dis(f)
# LOAD_DEREF x   ← loads through the cell pointer

The key bytecode instructions: MAKE_CELL allocates the heap-based cell; STORE_DEREF writes to the cell; LOAD_DEREF reads from it. These are distinct from the stack-based STORE_FAST/LOAD_FAST used for regular local variables, because the cell must outlive the outer function's stack frame.

✅ Expert tab complete

  • I know Python allocates cell objects on the heap (not the stack) for captured variables
  • I know STORE_DEREF and LOAD_DEREF are the bytecode instructions for cell variable access
  • I can use dis.dis() to see the difference between a closure variable and a regular local

Continue to Decorators →

Sources

1
Python Language Reference §4.2.2 — Naming and binding: free variables, cell variables, and closures. docs.python.org/3/reference/executionmodel.html.
2
Python Language Reference §4.2.4 — Interaction with dynamic features. docs.python.org/3/reference/executionmodel.html.
3
Python Data Model § — __closure__ attribute. docs.python.org/3/reference/datamodel.html#index-55.
4
Python Built-in Functions — nonlocal statement. docs.python.org/3/reference/simple_stmts.html#the-nonlocal-statement.
Source confidence: High Last verified: Primary source: Python Language Reference §4.2