Python Basics — Closures
Closures
A closure is a function that remembers the variables from its enclosing scope, even after that scope has finished executing. Closures are one of Python’s most elegant features.
What Is a Closure?
A closure occurs when:
- You have a nested function (a function inside another function)
- The inner function references variables from the outer function
- The outer function returns the inner function
def make_greeter(greeting):
def greeter(name):
return f"{greeting}, {name}!" # greeting is "closed over"
return greeter
hello = make_greeter("Hello")
namaste = make_greeter("Namaste")
print(hello("Alice")) # Hello, Alice!
print(namaste("Bob")) # Namaste, Bob!
After make_greeter returns, greeting should be gone — but the inner function remembers it. That’s a closure.
Free Variables and Cell Objects
The variable greeting in the example above is called a free variable — it’s not local to the inner function, nor is it global. Python stores it in a special cell object:
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
# Inspect the closure
print(double.__code__.co_freevars) # ('factor',)
print(double.__closure__) # (<cell at 0x...: int object at 0x...>,)
print(double.__closure__[0].cell_contents) # 2
Closure Use Cases
Data Hiding
Closures can create private state without classes:
def make_bank_account(initial_balance):
balance = initial_balance
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
if amount > balance:
return "Insufficient funds"
balance -= amount
return balance
def get_balance():
return balance
return deposit, withdraw, get_balance
deposit, withdraw, check = make_bank_account(1000)
print(deposit(500)) # 1500
print(withdraw(200)) # 1300
print(check()) # 1300
# No way to access 'balance' directly — it's hidden!
Function Factories
Create specialized functions on-the-fly:
def make_power(exponent):
def power(base):
return base ** exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(5)) # 25
print(cube(5)) # 125
Logging / Tracking
def make_logger(prefix):
def log(message):
print(f"[{prefix}] {message}")
return log
info = make_logger("INFO")
error = make_logger("ERROR")
info("Server started") # [INFO] Server started
error("Connection failed") # [ERROR] Connection failed
Caching (Memoization)
def make_cached(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
def slow_square(n):
"""Simulates an expensive computation."""
return n ** 2
fast_square = make_cached(slow_square)
print(fast_square(10)) # 100 — computed
print(fast_square(10)) # 100 — from cache
The __closure__ Attribute
Every function has a __closure__ attribute. It’s None for regular functions and a tuple of cell objects for closures:
def regular():
return 42
def make_closure():
x = 10
def inner():
return x
return inner
print(regular.__closure__) # None
print(make_closure().__closure__) # (<cell at 0x...>,)
Closures vs Classes
Both closures and classes can maintain state. When should you use which?
| Feature | Closure | Class |
|---|---|---|
| Simple state (1-2 vars) | Preferred | Overkill |
| Complex state | Harder to manage | Preferred |
| Multiple methods | Awkward (return tuple) | Natural |
| Readability | Great for small patterns | Better for complex logic |
| Memory | Slightly lighter | Slightly heavier |
# Closure approach — great for simple cases
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
# Class approach — better for complex cases
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
def reset(self):
self.count = 0
Common Pitfall: Late Binding in Loops
# ❌ Surprise — all functions return 4!
functions = []
for i in range(5):
functions.append(lambda: i)
print([f() for f in functions]) # [4, 4, 4, 4, 4]
The lambda closes over the variable i, not its value. By the time the lambdas run, i is 4.
Fix: Default Argument
# ✅ Capture the current value with a default argument
functions = []
for i in range(5):
functions.append(lambda i=i: i)
print([f() for f in functions]) # [0, 1, 2, 3, 4]
Summary
- A closure is a function that remembers variables from its enclosing scope
- Three requirements: nested function, references outer variable, outer returns inner
- Free variables are stored in
__closure__cell objects - Use cases: data hiding, function factories, logging, caching
- Closures are best for simple state; use classes for complex state
- Watch out for late binding in loops — use default arguments to fix
Next up: Scope Best Practices →