Closures in Python

8/25/2020

Introduction

If we want to execute a code multiple times, we often utilize functions. So, what are functions? Essentially, they are wrappers that encompass a reusable block of code, allowing it to be executed multiple times. Functions have their own scope, and anything declared locally inside a function will only exist during each invocation of that function. Each call to a function will have different instances for the local variables in the function.

You might ask, is there any way for a function to store state between different calls to the function? Yes, we can accomplish this by using closures. Simply put, a closure is a record storing a function along with its environment. The environment exists for all calls of the function.

Now, why might you need this? We could achieve states between different calls of a function by means of global state, but they are not restricted or localized to the function, so they can be modified from other places, altering the intended behaviour of the function. Also, keeping multiple global variables for a function is hardly maintainable.

Why we need closure?

Let’s delve into a practical example. We’ll try to implement a function that will only execute the passed function once, regardless of the number of times it is called. In Python, we can pass a function to another function as they are treated as objects.

is_called = False

def once(fun):
    def inner_func():
        global is_called
        if not is_called:
            is_called = True
            fun()
    return inner_func

def greet():
    print("Hi")

greet_once = once(greet)
greet_once()
greet_once()

Multiple calls to greet_once() will only execute greet() once, as is_called becomes True after the first call. However, as is_called is declared as global, other code or programmers can modify our function’s behaviour by changing the value of is_called, which is something we don’t want.

What if we want to restrict the execution of another function bye() once regardless of calls to it? We can achieve this by modifying the above code in the following manner:

is_called = {}

def once(fun, func_name):
    def inner_func():
        global is_called
        if not is_called.get(func_name):
            is_called[func_name] = True
            fun()
    return inner_func

def greet():
    print("Hi")

def bye():
    print("Bye")

greet_once = once(greet, 'greet')
greet_once()
greet_once()

bye_once = once(bye, 'bye')
bye_once()
bye_once()

We need to maintain a different state for each function - greet() and bye(). Unfortunately, the state of each function, is_called[func_name], is not isolated from each other. We can access states of other functions from our function, leading to potential conflicts.

With closure, we are able to avoid these problems.

Using closures

There are 3 types of scopes in Python - global, local, non-local scopes.

  • global - defines the scope of a name at module level
  • local - default, used only in that specific scope
  • non-local - for variable from enclosing scope like in case of nested function.

For a closure to be created,

  • The function should be called outside the scope it is created. This can be achieved by creating a function inside another function, returning it, and then calling the returned function.
  • Function should access variables that are non-local to its scope. We can achieve this accessing variables defined in the enclosing function of the returned nested function.

A closure is created everytime an inner function using a non-local variable is returned. If a variable is used in a code block, but not defined in its scope and it is not global, then the variable is a free variable. If a function does not use free variables it doesn’t form a closure. The function attributes func_closure in Python before 3.x or __closure__ in Python after 3.x contains tuple of cells which contain the free variables of the function. The current value of each free variable can be accessed by cell_contents attribute of cell. The closure will access the free variables from the cell. Every function in Python has this closure attributes, but it doesn’t save any content in cell if there is no free variables.

>>> x = 0
>>> def foo():
...     y = 1
...     def bar():
...         print(x)
...         print(y)
...     return bar
...
>>> foo().__code__.co_freevars
('y',)

For the function bar returned by foo(), only y is a free variable.

Building on this understanding, let’s implement the once function using closure.

def once(func):
    is_called = False
    def inner_func(arg):
        nonlocal is_called
        if not is_called:
            is_called = True
            func(arg)
    return inner_func


once_print = once(print)
once_print('joel')
once_print('jacob')

another_once_print = once(print)
another_once_print('python')
another_once_print(3)

When we run the above code, the outcome is that only “joel” and “python” are printed. As the boolean state is_called is local to each once, we don’t need to maintain different states for each instance of the returned inner function.

In Python 2, because there is no nonlocal keyword, we can’t change the binding of a non-local name, resulting in only read closure support. However, we can still have write support for the state of a closure by using mutable data types like lists and mutating them, although this approach might be considered a bit hacky. Here’s how we could implement a closure with write support in Python 2:

def once(func):
    is_called = [False]
    def inner_func(arg):
        if not is_called[0]:
            func(arg)
            is_called[0] = True
    return inner_func

Conclusion

By using closures, we achieve a more elegant design, allowing us to eliminate the need for global variables, thus making the code more readable and maintainable. We are able to prevent duplication of code, and facilitate the creation of common functionality across different parts of our program. Decorators are great examples of closures that enable us to write common code and prevent its duplication.

So, in a nutshell, closures are a powerful tool that helps in managing state and code modularity. I hope you enjoyed the article, and it helped you grasp a deeper understanding of closures. I’m eager to hear your thoughts and engage in further discussion in the comments!

© 2025 Joel Jacob