Python Closures: Nested Functions and Free Variables
A closure is a function that captures and remembers variables from its enclosing scope, even after that scope has finished executing. In Python, closures emerge naturally from the combination of...
Key Insights
- Closures enable functions to remember and access variables from their enclosing scope even after that scope has finished executing, making them powerful tools for creating function factories and maintaining private state
- The
nonlocalkeyword is essential when you need to modify (not just read) variables from an enclosing scope, but forgetting it leads to one of Python’s most common closure bugs - While closures and classes can solve similar problems, closures excel at simple state encapsulation with minimal boilerplate, whereas classes are better for complex objects with multiple methods and inheritance needs
Introduction to Closures
A closure is a function that captures and remembers variables from its enclosing scope, even after that scope has finished executing. In Python, closures emerge naturally from the combination of nested functions and lexical scoping rules. When an inner function references variables from an outer function, it “closes over” those variables, preserving their values for later use.
Here’s the simplest possible closure:
def outer(message):
def inner():
print(message) # 'message' is a free variable
return inner
greeting = outer("Hello, closures!")
greeting() # Output: Hello, closures!
Even though outer() has finished executing, the inner() function still has access to the message variable. This behavior isn’t magic—Python stores references to these captured variables within the function object itself.
Anatomy of a Closure
For a proper closure to exist, three conditions must be met:
- You must have a nested function (a function defined inside another function)
- The inner function must reference variables from the enclosing scope (these are called “free variables”)
- The inner function must be returned or passed elsewhere, outliving its enclosing scope
Let’s contrast a closure with a regular nested function:
# Regular nested function - NOT a closure
def calculator():
def add(a, b):
return a + b
result = add(5, 3)
return result
# Closure - inner function references 'multiplier' from enclosing scope
def make_multiplier(multiplier):
def multiply(number):
return number * multiplier # 'multiplier' is a free variable
return multiply
times_three = make_multiplier(3)
print(times_three(10)) # Output: 30
The key difference: in the closure example, multiply references multiplier from its enclosing scope and is returned, allowing it to be called later with the captured value intact.
You can inspect a function’s closure using the __closure__ attribute:
def create_counter(start):
count = start
def increment():
nonlocal count
count += 1
return count
return increment
counter = create_counter(10)
print(counter.__closure__) # (<cell at 0x...: int object at 0x...>,)
print(counter.__closure__[0].cell_contents) # Output: 10
Practical Use Cases
Closures shine in several real-world scenarios. Let’s explore the most common patterns.
Function Factories
Closures excel at creating specialized functions based on configuration:
def make_counter(start=0, step=1):
count = start
def increment():
nonlocal count
count += step
return count
return increment
counter_by_ones = make_counter(0, 1)
counter_by_fives = make_counter(0, 5)
print(counter_by_ones()) # 1
print(counter_by_ones()) # 2
print(counter_by_fives()) # 5
print(counter_by_fives()) # 10
Configuration-Based Validators
Closures can encapsulate validation logic with configuration:
def make_validator(min_value, max_value):
def validate(value):
if not isinstance(value, (int, float)):
return False
return min_value <= value <= max_value
return validate
age_validator = make_validator(0, 120)
percentage_validator = make_validator(0, 100)
print(age_validator(25)) # True
print(age_validator(150)) # False
print(percentage_validator(50)) # True
Decorators
Most Python decorators are implemented using closures:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
# Output:
# Calling add with args=(3, 5), kwargs={}
# add returned 8
The nonlocal Keyword
Reading free variables from an enclosing scope works automatically, but modifying them requires the nonlocal keyword. This is a common source of bugs.
Here’s the problem without nonlocal:
def broken_counter():
count = 0
def increment():
count += 1 # UnboundLocalError!
return count
return increment
# This will raise an error when called
Python sees the assignment count += 1 and assumes count is a local variable. Since it’s referenced before assignment, you get an error.
The fix is straightforward:
def working_counter():
count = 0
def increment():
nonlocal count # Explicitly declare we're modifying the enclosing scope
count += 1
return count
return increment
counter = working_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
Important distinction: you only need nonlocal for reassignment. Mutating mutable objects works without it:
def list_appender():
items = []
def append(item):
items.append(item) # No nonlocal needed - we're mutating, not reassigning
return items
return append
appender = list_appender()
print(appender(1)) # [1]
print(appender(2)) # [1, 2]
Closures vs. Classes
Closures and classes can solve similar problems. Here’s the same functionality implemented both ways:
# Closure approach
def make_account(initial_balance):
balance = initial_balance
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
if amount > balance:
raise ValueError("Insufficient funds")
balance -= amount
return balance
def get_balance():
return balance
return {'deposit': deposit, 'withdraw': withdraw, 'balance': get_balance}
# Class approach
class Account:
def __init__(self, initial_balance):
self._balance = initial_balance
def deposit(self, amount):
self._balance += amount
return self._balance
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
return self._balance
def get_balance(self):
return self._balance
# Usage is similar
closure_account = make_account(100)
class_account = Account(100)
print(closure_account['deposit'](50)) # 150
print(class_account.deposit(50)) # 150
When to use closures:
- Simple state encapsulation with one or two functions
- Function factories and configuration-based behavior
- Decorators and callbacks
- When you want minimal boilerplate
When to use classes:
- Complex objects with many methods
- Need inheritance or polymorphism
- Multiple instances with shared behavior
- When clarity and conventional OOP patterns matter more than brevity
Common Pitfalls and Best Practices
The Late Binding Loop Problem
This is the classic closure gotcha:
# WRONG - all functions reference the same 'i'
functions = []
for i in range(3):
def func():
return i
functions.append(func)
print([f() for f in functions]) # [2, 2, 2] - not [0, 1, 2]!
The problem: all closures share the same i variable, which has value 2 after the loop completes.
Solution 1: Default argument
functions = []
for i in range(3):
def func(x=i): # Capture current value of i
return x
functions.append(func)
print([f() for f in functions]) # [0, 1, 2]
Solution 2: functools.partial
from functools import partial
def return_value(x):
return x
functions = [partial(return_value, i) for i in range(3)]
print([f() for f in functions]) # [0, 1, 2]
Memory Considerations
Closures keep references to their enclosing scope, which can prevent garbage collection:
def create_large_closure():
large_data = [0] * 1000000 # 1 million integers
def small_function():
return "I don't even use large_data"
return small_function
func = create_large_closure()
# large_data is still in memory because of the closure!
If you don’t need certain variables, delete them explicitly or restructure your code.
Closures are a fundamental Python feature that enables elegant solutions for state management, function factories, and decorators. Master them, and you’ll write more expressive, functional-style Python code. Just remember to use nonlocal when modifying enclosing variables, watch out for late binding in loops, and choose between closures and classes based on complexity and clarity needs.