22.2 Free Variables and __closure__
In Python, a free variable is a variable that is used in a function’s code block but is not defined there and is not a global variable. Instead, it is “captured” or “bound” from an enclosing function’s scope. This mechanism is the very foundation of a closure, a function object that remembers values in enclosing scopes even if those scopes are no longer present in memory.
The Anatomy of a Closure
A closure is created when a nested function references a value from its enclosing scope, and that nested function is returned to a context where the enclosing scope has ceased to exist. The closure “closes over” the free variable, preserving it for later use.
def outer_function(msg):
# `message` is a local variable of `outer_function`
message = msg
def inner_function():
# `message` is a FREE VARIABLE for `inner_function`
print(message)
# Return the inner function, which has captured `message`
return inner_function
# Call the outer function, which executes and then finishes.
# Its local scope (frame) would normally be destroyed.
my_func = outer_function("Hello, Closure!")
# Yet, when we call the inner function, it still has access to `message`.
my_func() # Output: Hello, Closure!
Here’s why this works: inner_function is not just a function; it’s a closure. It carries with it a reference to the free variable message from the scope of outer_function, even after outer_function has finished executing.
Inspecting the closure Attribute
Every function object in Python has a __closure__ attribute. For a normal function defined at the module level, this attribute is None. However, for a closure, __closure__ is a tuple of cell objects that contain references to the free variables. Each cell has a cell_contents attribute which holds the actual value.
def make_counter(initial_value=0):
count = initial_value # This will become a free variable
def counter():
nonlocal count # Necessary to modify the free variable
count += 1
return count
return counter
# Create two independent counters
counter_a = make_counter(5)
counter_b = make_counter(100)
# Inspect the closure of counter_a
print(counter_a.__closure__) # Output: (<cell at 0x...: int object at 0x...>,)
print(counter_a.__closure__[0].cell_contents) # Output: 5 (the initial value)
# Call the counters and see their state change
print(counter_a()) # Output: 6
print(counter_a.__closure__[0].cell_contents) # Output: 6 (the value was updated)
print(counter_b()) # Output: 101
This introspection reveals the hidden state that the closure maintains. Each call to make_counter creates a new, separate scope for count, resulting in independent closures for counter_a and counter_b.
The Crucial Role of the nonlocal Keyword
Attempting to rebind (assign a new value to) a free variable without the nonlocal keyword leads to a common pitfall. Python interprets the assignment as the creation of a new local variable within the nested function, shadowing the intended free variable and causing an UnboundLocalError if the variable is read before the assignment.
def broken_counter():
count = 0
def increment():
# Without `nonlocal`, this creates a new local `count`.
# Trying to read it *before* assignment causes an error.
count = count + 1 # UnboundLocalError: local variable 'count' referenced before assignment
return count
return increment
broken = broken_counter()
# broken() # This would raise an UnboundLocalError
The nonlocal statement explicitly declares a variable as a free variable, allowing you to rebind it. It tells the Python interpreter to look for the variable in the nearest enclosing scope that is not global.
def working_counter():
count = 0
def increment():
nonlocal count # Declare `count` as a free variable to be rebound
count += 1
return count
return increment
fixed = working_counter()
print(fixed()) # Output: 1
print(fixed()) # Output: 2
It’s critical to understand that nonlocal does not create a new variable; it only allows an existing variable from an enclosing non-global scope to be rebound.
Common Pitfalls and Best Practices
Accidental Shadowing: The most common mistake is forgetting the
nonlocalkeyword when assigning to a free variable, which silently creates a new local variable instead of modifying the intended one. Always usenonlocalfor rebinding.Cyclic References and Memory Leaks: Closures create strong references to their captured variables. If a closure is stored in a long-lived object (e.g., a class instance) and it captures a reference to that same large object, it creates a cyclic reference that can prevent garbage collection. Be mindful of what your closures capture.
Overuse and Readability: While powerful, closures can make code less readable if overused or if they capture many variables from deeply nested scopes. For complex state, a class is often a more explicit and maintainable alternative. A class with a
__call__method can emulate a closure’s behavior while being more transparent about its internal state.nonlocalScope Search: Thenonlocalkeyword searches enclosing scopes from the innermost outward until it finds a variable with the given name. It will not search the global scope. If no matching variable is found, aSyntaxErroris raised at compile time.
def outer():
x = "outer"
def inner():
def innermost():
nonlocal x # This correctly finds `x` in `outer`'s scope
x = "modified"
innermost()
print(f"Inner: {x}")
inner()
print(f"Outer: {x}")
outer()
# Output:
# Inner: modified
# Outer: modified