Python Closures

Introduction

Python closures are functions bundled up with variables they access in outer, non-local scopes of enclosing functions. Closures are essential when inner functions need access to variables from their enclosing scope because without them, these variables would be lost once the enclosing function completes execution.

The Inner Functions lesson touched upon closures since they are created as inner functions, and this lesson will take a more detailed look at closures.

Why Are Closures Needed?

A function’s local variables (including function arguments) are not accessible from outside that function. When a function finishes executing, its local variables are lost (i.e. destroyed). This would be a problem for inner functions that access those variables and are then are returned by its outer function.

The next example shows an inner function that still needs to preserve variables it accesses from an outer function, even after the latter finished executing.

Example:

def message_generator():
    message = "Hello, I'm a variable in message_generator()."
    
    def message_printer():
         print(message)
         
    return message_printer

my_function = message_generator()
my_function()

Output:

Hello, I'm a variable in message_generator().

In the example above, the outer function message_generator() returns an inner function message_printer() (line 7). The message_printer() function uses the variable message from message_generator()‘s scope to print a message. When message_generator() executes (line 8), all it does is return message_printer(). The variable my_message_fn is assigned to that function, and therefore, calling my_message_fn() (line 9) executes the code in message_printer(). At this point, message_generator() had already finished executing, and therefore its local scope is no longer valid. The closure preserves the message variable despite the demise of its original scope.

How Do Closures Work?

Python creates a closure when an inner function accesses one or more variables form an outer function scope (a.k.a. non-local scope). To do so, Python creates a data structure called a cell that references the non-local variables the closure’s function accesses. This way, the variables are preserved after the outer function had terminated.

Python makes the cell accessible through a function’s __closure__ property. You don’t normally need to access it when working with closures, but it provides insight into how a closure is created.

In the next example we’ll inspect __closure__ with two inner functions: one that is a closure because it accesses a non-local variable, and another that is not because it does not access a non-local variable.

Example:

def returns_a_closure():
    message = "Hello, I'm a variable in returns_a_closure()."
    
    def i_am_a_closure():
        print(message)
      
    return i_am_a_closure 
   
def returns_a_function():
    def i_am_just_a_function():
        print("Hello, I'm just a message directly from i_am_just_a_function().")
    
    return i_am_just_a_function

cl = returns_a_closure()
fn = returns_a_function()

print(cl.__closure__)
print(fn.__closure__)

Output:

(<cell at 0x7fad4fac73a0: str object at 0x7fad4fabca50>,)
None

In the example above, returns_a_closure() is function that returns the closure i_am_a_closure() (line 7). Python turns the i_am_a_closure() function into a closure because it accesses message (line 5), a non-local variable defined by its parent function (line 2). On the other hand, returns_a_function() returns the function i_am_just_a_function(), which does not access any non-local variables and is therefore a mere function and not a closure.

At the end, the code prints the closure’s cell object (line 19). The resulting printout includes the cell’s memory address, as well as the type (str) and memory address of the message variable it contains. On the following line (line 20), it prints out the __closure__ property of the non-closure function, but the result is None since it does not have a cell.

Free Variables

A free variable is term that refers to a non-local variable accessed by an inner function. Such a variable is free because it is not bound to a specific single scope. In the previous example, message is a free variable accessed by the inner function i_am_a_closure() (line 5).

You can get a tuple containing the names of a closure’s free variables using the __code__.co_freevars property.

Example:

def greeting_generator():
    free_var1 = "Hello"
    free_var2 = "there"
    
    def greet_fn():
         print(f"{free_var1} {free_var2}!")
         
    return greet_fn

greet = greeting_generator()
greet()
print("My free variables are:", greet.__code__.co_freevars)

Output:

Hello there!
My free variables are: ('free_var1', 'free_var2')

In the example above, the function greeting_generator() defines two variables free_var1 and free_var2 (lines 2 and 3). The inner function greet_fn() accesses them as free variables since they are defined in a non-local scope (line 6). Finally, the code gets the closure from greeting_generator() (line 10), executes it to print the greeting (line 11), and prints the names of its free variables.

Summary & Reference for Python Closures

Python closures are functions bundled up with variables they access in outer, non-local scopes of enclosing functions. Closures are essential when inner functions need access to variables from their enclosing scope because without them, these variables would be lost once the enclosing function completes execution.


Python creates a closure when an inner function accesses a non-local variable form an outer function scope.

def message_generator():
    message = "Hello, I'm a variable in message_generator()."
    
    def message_printer():  # <-- closure
         print(message)  # <-- `message` is from an outer, non-local scope
         
    return message_printer

my_function = message_generator()
my_function()  # --> Hello, I'm a variable in message_generator().

A closure include a data structure called a cell that references its outer scope variables. You can access the cell object using the closure’s __closure__ property.

def returns_a_closure():
    message = "Hello, I'm a variable in returns_a_closure()."
    
    def i_am_a_closure():
        print(message)
      
    return i_am_a_closure 

cl = returns_a_closure()

print(cl.__closure__)  # --> (<cell at 0x7f894f27ba90: str object at 0x7f894f1bb930>,)

Non-local variables associated with a closure are called free variables. You can get a tuple containing the names of a closure’s free variables with the __code__.co_freevars property.

def greeting_generator():
    free_var1 = "Hello"
    free_var2 = "there"
    
    def greet_fn():
         print(f"{free_var1} {free_var2}!")
         
    return greet_fn

greet = greeting_generator()
print("Free variables:", greet.__code__.co_freevars)  
# --> Free variables: ('free_var1', 'free_var2')