Python Closures - Quick Reference Cheat Sheet¶
Definition¶
A closure is a function that: 1. Is defined inside another function (nested) 2. References variables from the outer function's scope 3. Can be returned and called later, still remembering those variables
def outer(x):
def inner(y):
return x + y # inner "closes over" x
return inner
add_5 = outer(5) # add_5 is a closure
print(add_5(3)) # 8
Basic Template¶
def outer_function(outer_var):
# outer_var is captured by the closure
def inner_function(inner_var):
# Can access both outer_var and inner_var
return outer_var + inner_var
return inner_function # Return without calling ()
# Create closure
my_closure = outer_function(10)
result = my_closure(5) # 15
The nonlocal Keyword¶
Use nonlocal to modify variables from the enclosing scope:
def make_counter():
count = 0
def increment():
nonlocal count # Required to modify count
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
Without nonlocal: - Can READ outer variables β - Cannot MODIFY outer variables β (creates local variable instead)
With nonlocal: - Can both READ and MODIFY outer variables β
Common Patterns¶
1. Factory Functions¶
Create specialized functions:
def make_multiplier(factor):
def multiply(n):
return n * factor
return multiply
times_2 = make_multiplier(2)
times_10 = make_multiplier(10)
2. Data Encapsulation (Private Variables)¶
Hide implementation details:
def make_account(balance):
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
balance -= amount
return balance
def get_balance():
return balance
return {'deposit': deposit, 'withdraw': withdraw, 'balance': get_balance}
3. Configuration Functions¶
Store settings:
def make_formatter(prefix, suffix):
def format(text):
return f"{prefix}{text}{suffix}"
return format
html_bold = make_formatter("<b>", "</b>")
html_italic = make_formatter("<i>", "</i>")
4. Callbacks with State¶
Event handlers that remember:
def make_click_handler(element_id):
click_count = 0
def handle_click():
nonlocal click_count
click_count += 1
print(f"{element_id}: {click_count} clicks")
return handle_click
5. Memoization/Caching¶
Cache expensive results:
def make_memoized(func):
cache = {}
def memoized(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return memoized
6. Counters and Accumulators¶
Maintain state between calls:
def make_counter(start=0):
count = start
def counter():
nonlocal count
count += 1
return count
return counter
LEGB Rule (Variable Lookup Order)¶
Python searches for variables in this order: 1. Local: Inside current function 2. Enclosing: In enclosing functions (closures!) 3. Global: Module level 4. Built-in: Python built-ins
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # Prints "local"
inner()
Multiple Closures Sharing State¶
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
def decrement():
nonlocal count
count -= 1
return count
def get():
return count
return increment, decrement, get
inc, dec, get = make_counter()
Common Pitfall: Closures in Loops¶
β WRONG:¶
def create_funcs():
funcs = []
for i in range(3):
funcs.append(lambda x: x * i)
return funcs
f = create_funcs()
print(f[0](2)) # Expected: 0, Got: 4
print(f[1](2)) # Expected: 2, Got: 4
print(f[2](2)) # Expected: 4, Got: 4
# All closures share the same 'i' which is 2 after loop!
β SOLUTION 1: Default Argument¶
def create_funcs():
funcs = []
for i in range(3):
funcs.append(lambda x, i=i: x * i) # Capture current i
return funcs
β SOLUTION 2: Factory Function¶
def make_multiplier(i):
return lambda x: x * i
def create_funcs():
return [make_multiplier(i) for i in range(3)]
Closures vs Classes¶
Use Closure When:¶
- β Simple, single-method behavior
- β Private data without class overhead
- β Functional programming style
- β Quick factory functions
Use Class When:¶
- β Multiple related methods
- β Complex state management
- β Need inheritance
- β Need special methods (
__str__,__repr__, etc.)
Example Comparison:¶
Closure:
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
Class:
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
Inspecting Closures¶
def outer(x):
def inner(y):
return x + y
return inner
f = outer(5)
# See captured variables
print(f.__closure__) # (<cell at 0x...: int object at 0x...>,)
print(f.__code__.co_freevars) # ('x',)
# Get value
print(f.__closure__[0].cell_contents) # 5
Decorators (Built on Closures)¶
Decorators are just closures used to wrap functions:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@my_decorator
def greet(name):
print(f"Hello, {name}!")
Partial Application¶
Create specialized functions from general ones:
def partial(func, *fixed_args):
def wrapper(*args):
return func(*fixed_args, *args)
return wrapper
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
Function Composition¶
Combine functions:
def compose(f, g):
def composed(x):
return f(g(x))
return composed
def add_10(x):
return x + 10
def multiply_2(x):
return x * 2
add_then_multiply = compose(multiply_2, add_10)
print(add_then_multiply(5)) # 30: (5 + 10) * 2
Best Practices¶
β DO:¶
- Keep closures simple and focused
- Use descriptive names for captured variables
- Document what the closure captures
- Use
nonlocalonly when necessary - Consider readability
β DON'T:¶
- Create deeply nested closures (2-3 levels max)
- Capture mutable objects without care
- Use closures for complex state (use classes)
- Forget about the loop variable pitfall
- Sacrifice clarity for cleverness
Quick Reference Table¶
| Feature | Syntax | Use Case |
|---|---|---|
| Basic closure | def outer(): def inner(): pass |
Remember variables |
| Modify outer var | nonlocal var |
Change enclosing scope |
| Multiple returns | return f1, f2, f3 |
Multiple closures |
| Private data | Return dict of functions | Encapsulation |
| Factory | make_thing(config) |
Specialized functions |
| Decorator | def deco(func): def wrap(): pass |
Wrap functions |
Common Use Cases Summary¶
# 1. Counter
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
# 2. Adder
def make_adder(n):
return lambda x: x + n
# 3. Formatter
def make_formatter(pre, suf):
return lambda text: f"{pre}{text}{suf}"
# 4. Range checker
def make_range_checker(min, max):
return lambda x: min <= x <= max
# 5. Cache
def make_cached(func):
cache = {}
def cached(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return cached
Memory Tip¶
Closures = Functions + Their Environment
Think of closures as a "backpack" that the function carries around, containing all the variables it needs from its birthplace.
Key Insight: Closures are not just about nested functions - they're about functions that remember their environment. Master this concept, and decorators, callbacks, and functional programming patterns become much easier!
Runnable Example: closures_mini_project.py¶
"""
Closures Mini-Project: Event Management System
This project demonstrates how to use closures to create a simple event
management system with event handlers, state management, and callbacks.
"""
import time
from datetime import datetime
if __name__ == "__main__":
print("=" * 70)
print("CLOSURES MINI-PROJECT: EVENT MANAGEMENT SYSTEM")
print("=" * 70)
# ============================================================================
# EVENT EMITTER USING CLOSURES
# ============================================================================
print("\n1. EVENT EMITTER")
print("-" * 70)
def create_event_emitter():
"""
Create an event emitter that allows subscribing to events and
triggering callbacks. Uses closures to maintain the list of listeners.
"""
listeners = {} # Private to the closure
def on(event_name, callback):
"""Subscribe to an event"""
if event_name not in listeners:
listeners[event_name] = []
listeners[event_name].append(callback)
print(f"β Subscribed to '{event_name}'")
def off(event_name, callback):
"""Unsubscribe from an event"""
if event_name in listeners and callback in listeners[event_name]:
listeners[event_name].remove(callback)
print(f"β Unsubscribed from '{event_name}'")
def emit(event_name, *args, **kwargs):
"""Trigger an event and call all subscribers"""
if event_name in listeners:
print(f"π’ Emitting '{event_name}' event")
for callback in listeners[event_name]:
callback(*args, **kwargs)
else:
print(f"β οΈ No listeners for '{event_name}'")
def get_listener_count(event_name=None):
"""Get number of listeners"""
if event_name:
return len(listeners.get(event_name, []))
return sum(len(v) for v in listeners.values())
return {
'on': on,
'off': off,
'emit': emit,
'listener_count': get_listener_count
}
# Create an event emitter
emitter = create_event_emitter()
# Create event handlers using closures
def make_user_handler(user_id):
"""Factory function to create user-specific handlers"""
login_count = 0
def on_login():
nonlocal login_count
login_count += 1
print(f" User {user_id} logged in (total: {login_count} times)")
def on_logout():
print(f" User {user_id} logged out")
return on_login, on_logout
# Subscribe to events
user1_login, user1_logout = make_user_handler("user_001")
user2_login, user2_logout = make_user_handler("user_002")
emitter['on']('user_login', user1_login)
emitter['on']('user_logout', user1_logout)
emitter['on']('user_login', user2_login)
print(f"Total listeners: {emitter['listener_count']()}")
# Trigger events
print("\nSimulating user activity:")
emitter['emit']('user_login')
emitter['emit']('user_login')
emitter['emit']('user_logout')
# ============================================================================
# STATE MACHINE USING CLOSURES
# ============================================================================
print("\n\n2. STATE MACHINE")
print("-" * 70)
def create_state_machine(initial_state, transitions):
"""
Create a state machine using closures.
Maintains current state privately.
"""
current_state = initial_state
history = [initial_state]
def get_state():
"""Get current state"""
return current_state
def transition(event):
"""Attempt to transition to a new state"""
nonlocal current_state
if current_state in transitions and event in transitions[current_state]:
new_state = transitions[current_state][event]
print(f" {current_state} --[{event}]--> {new_state}")
current_state = new_state
history.append(current_state)
return True
else:
print(f" β οΈ Invalid transition: {event} from {current_state}")
return False
def get_history():
"""Get state history"""
return history.copy()
def reset():
"""Reset to initial state"""
nonlocal current_state
current_state = initial_state
history.clear()
history.append(initial_state)
print(f" Reset to {initial_state}")
return {
'state': get_state,
'transition': transition,
'history': get_history,
'reset': reset
}
# Create a door state machine
door_transitions = {
'closed': {'open': 'open'},
'open': {'close': 'closed'},
}
door = create_state_machine('closed', door_transitions)
print(f"Initial state: {door['state']()}")
print("\nDoor operations:")
door['transition']('open')
door['transition']('close')
door['transition']('close') # Invalid
door['transition']('open')
print(f"\nHistory: {' -> '.join(door['history']())}")
# ============================================================================
# RATE-LIMITED API CLIENT
# ============================================================================
print("\n\n3. RATE-LIMITED API CLIENT")
print("-" * 70)
def create_rate_limited_client(max_requests, time_window):
"""
Create an API client with rate limiting using closures.
Tracks request times privately.
"""
request_times = []
request_count = 0
def make_request(endpoint):
"""Make an API request with rate limiting"""
nonlocal request_count
now = time.time()
# Remove old requests outside the time window
request_times[:] = [t for t in request_times if now - t < time_window]
if len(request_times) >= max_requests:
wait_time = time_window - (now - request_times[0])
print(f" βΈοΈ Rate limit reached. Wait {wait_time:.1f}s")
return None
request_times.append(now)
request_count += 1
print(f" β Request #{request_count} to {endpoint}")
return {"status": "success", "endpoint": endpoint}
def get_stats():
"""Get request statistics"""
return {
'total_requests': request_count,
'recent_requests': len(request_times),
'limit': max_requests,
'window': time_window
}
def reset_stats():
"""Reset request statistics"""
nonlocal request_count
request_times.clear()
request_count = 0
print(" Stats reset")
return {
'request': make_request,
'stats': get_stats,
'reset': reset_stats
}
# Create a rate-limited client (3 requests per 1 second)
api_client = create_rate_limited_client(max_requests=3, time_window=1.0)
print("Making API requests (max 3 per second):")
for i in range(5):
result = api_client['request'](f'/api/users/{i}')
time.sleep(0.2)
stats = api_client['stats']()
print(f"\nStats: {stats}")
# ============================================================================
# CONFIGURABLE VALIDATORS
# ============================================================================
print("\n\n4. CONFIGURABLE VALIDATORS")
print("-" * 70)
def create_validator(rules):
"""
Create a validator with configurable rules using closures.
"""
validation_count = 0
failed_count = 0
def validate(data):
"""Validate data against rules"""
nonlocal validation_count, failed_count
validation_count += 1
errors = []
for field, rule in rules.items():
if field not in data:
errors.append(f"Missing field: {field}")
elif not rule['check'](data[field]):
errors.append(f"{field}: {rule['message']}")
if errors:
failed_count += 1
return {'valid': False, 'errors': errors}
return {'valid': True, 'errors': []}
def get_stats():
"""Get validation statistics"""
return {
'total': validation_count,
'failed': failed_count,
'success_rate': f"{((validation_count - failed_count) / validation_count * 100):.1f}%"
if validation_count > 0 else "N/A"
}
return {
'validate': validate,
'stats': get_stats
}
# Create validators with different rules
user_rules = {
'username': {
'check': lambda x: len(x) >= 3,
'message': 'Must be at least 3 characters'
},
'email': {
'check': lambda x: '@' in x and '.' in x,
'message': 'Must be a valid email'
},
'age': {
'check': lambda x: isinstance(x, int) and 18 <= x <= 120,
'message': 'Must be between 18 and 120'
}
}
user_validator = create_validator(user_rules)
# Test validation
test_users = [
{'username': 'alice', 'email': 'alice@example.com', 'age': 25},
{'username': 'bo', 'email': 'invalid', 'age': 15},
{'username': 'charlie', 'email': 'charlie@test.com', 'age': 30},
]
print("Validating users:")
for user in test_users:
result = user_validator['validate'](user)
if result['valid']:
print(f" β {user['username']}: Valid")
else:
print(f" β {user['username']}: {', '.join(result['errors'])}")
print(f"\nValidation stats: {user_validator['stats']()}")
# ============================================================================
# CACHE WITH EXPIRATION
# ============================================================================
print("\n\n5. CACHE WITH EXPIRATION")
print("-" * 70)
def create_cache(ttl=5):
"""
Create a cache with time-to-live using closures.
"""
cache = {}
hits = 0
misses = 0
def get(key):
"""Get value from cache"""
nonlocal hits, misses
if key in cache:
value, timestamp = cache[key]
if time.time() - timestamp < ttl:
hits += 1
print(f" πΎ Cache HIT for '{key}'")
return value
else:
del cache[key]
print(f" β Cache EXPIRED for '{key}'")
misses += 1
print(f" β Cache MISS for '{key}'")
return None
def set(key, value):
"""Set value in cache"""
cache[key] = (value, time.time())
print(f" β Cached '{key}'")
def clear():
"""Clear all cache"""
cache.clear()
print(f" ποΈ Cache cleared")
def get_stats():
"""Get cache statistics"""
total = hits + misses
return {
'hits': hits,
'misses': misses,
'hit_rate': f"{(hits / total * 100):.1f}%" if total > 0 else "N/A",
'size': len(cache)
}
return {
'get': get,
'set': set,
'clear': clear,
'stats': get_stats
}
# Create cache with 2-second TTL
cache = create_cache(ttl=2)
print("Testing cache:")
cache['set']('user_123', {'name': 'Alice', 'age': 30})
cache['get']('user_123') # Hit
time.sleep(1)
cache['get']('user_123') # Hit
time.sleep(1.5)
cache['get']('user_123') # Expired
cache['set']('user_123', {'name': 'Alice', 'age': 30})
cache['get']('user_123') # Hit
print(f"\nCache stats: {cache['stats']()}")
# ============================================================================
# COMMAND PATTERN WITH UNDO
# ============================================================================
print("\n\n6. COMMAND PATTERN WITH UNDO")
print("-" * 70)
def create_command_manager():
"""
Create a command manager with undo/redo using closures.
"""
history = []
current_index = -1
def execute(command, undo_command):
"""Execute a command and save for undo"""
nonlocal current_index
command()
current_index += 1
# Remove any commands after current index (for redo)
history[:] = history[:current_index]
history.append((command, undo_command))
print(f" β Command executed (history size: {len(history)})")
def undo():
"""Undo last command"""
nonlocal current_index
if current_index >= 0:
_, undo_command = history[current_index]
undo_command()
current_index -= 1
print(f" βΆ Undo executed")
return True
print(f" β οΈ Nothing to undo")
return False
def redo():
"""Redo last undone command"""
nonlocal current_index
if current_index < len(history) - 1:
current_index += 1
command, _ = history[current_index]
command()
print(f" β· Redo executed")
return True
print(f" β οΈ Nothing to redo")
return False
def get_history_size():
"""Get size of command history"""
return len(history)
return {
'execute': execute,
'undo': undo,
'redo': redo,
'history_size': get_history_size
}
# Create command manager
cmd_manager = create_command_manager()
# Simulate a simple text editor
text = []
def add_text(word):
"""Command to add text"""
def do():
text.append(word)
print(f" Added: '{word}' -> {text}")
def undo():
text.pop()
print(f" Removed: '{word}' -> {text}")
cmd_manager['execute'](do, undo)
print("Text editor simulation:")
add_text("Hello")
add_text("World")
add_text("!")
print("\nUndo operations:")
cmd_manager['undo']()
cmd_manager['undo']()
print("\nRedo operations:")
cmd_manager['redo']()
# ============================================================================
# SUMMARY
# ============================================================================
print("\n" + "=" * 70)
print("PROJECT SUMMARY")
print("=" * 70)
print("""
This mini-project demonstrated practical closure usage:
β
Patterns Demonstrated:
1. Event Emitter - Subscribe/publish with private listener list
2. State Machine - Maintain state with transition rules
3. Rate Limiter - Track requests with time windows
4. Validators - Configurable validation with statistics
5. Cache - Time-based expiration with hit/miss tracking
6. Command Pattern - Undo/redo functionality
β
Key Closure Concepts:
- Private variables (listeners, state, cache, history)
- Multiple functions sharing state (nonlocal)
- Factory functions (make_user_handler)
- Encapsulation without classes
- Stateful behavior
β
Real-World Applications:
- Event-driven systems (UI frameworks, game engines)
- State management (workflows, processes)
- API clients with rate limiting
- Form validation systems
- Caching layers
- Undo/redo functionality (text editors, drawing apps)
β
Benefits of Using Closures:
- Clean, encapsulated code
- No class boilerplate needed
- Natural state management
- Easy to test individual components
- Functional programming style
Try extending this project by adding:
- Priority-based event emitters
- State machine with entry/exit actions
- LRU cache eviction policy
- Async validators
- Transaction support for commands
""")
print("=" * 70)
print("END OF MINI-PROJECT")
print("=" * 70)