Skip to main content
Python

Use closures for quick mini classes in Python

3 mins

A bottle sealed with a note inside, floating across the sea—representing a closure carrying its enclosed data (variables) even long after it’s been “sent” (the outer function finished executing)

What are closures? #

Closures in Python are a powerful feature that allows you to create functions that can capture and remember the environment in which they were created.

This means that you can create functions that have access to variables from their enclosing scope, even after that scope has finished executing.

Here’s a simple example of a closure to illustrate the concept.

def outer():
    x = 10
    def inner():
        print(x)
    return inner

closure_func = outer()
closure_func()  # prints 10

The general structure of a typical closed function is as follows:

def outer_function():
    # Enclosing scope
    variable = "I am from the outer function"

    def inner_function():
        # Inner function can access the variable from the outer function
        print(variable)

    return inner_function  # Return the inner function

Note that if the inner needs to modify the variable from the outer function, you will need to use the nonlocal keyword.

Replace simple classes with closures.

Classes are often used to encapsulate state and behavior.

If all you need in a class is to hold some state and a single method, you can use a closure instead.

nonlocal keyword #

When a variable in the outer function needs to be modified by the inner function, you must declare it as nonlocal.

See some examples of it’s use in the following use cases.

Counter Replacement #

Here’s an example of how you can use closures to create a counter class. This class simply increments a number each time it is called.

def make_id_counter(last_id=0):
    count = last_id

    def next_id():
        nonlocal count
        count += 1
        return count

    return next_id


next_item_id = make_id_counter(29)
next_product_id = make_id_counter(210)

print(next_item_id())
print(next_item_id())
print(next_product_id())

## Output:
# 30
# 31
# 211

Accumulator Replacement #

Use closures to hold a running total. The function parameter also acts as the equivalent of a class init method.

Note that since Python is not limited to a single return value, the inner function is returning the rolled value and the total.

import random

def make_dice(sides):
    total = 0

    def roll():
        nonlocal total
        rolled = random.randint(1, sides)
        total += rolled
        return rolled, total

    return roll


roll_six_sided = make_dice(6)
roll_ten_sided = make_dice(10)

roll6, total6 = roll_six_sided()
print("Rolled 6-sided die:", roll6, "Total:", total6)
roll10, total10 = roll_ten_sided()
print("Rolled 10-sided die:", roll10, "Total:", total10)
roll10, total10 = roll_ten_sided()
print("Rolled 10-sided die:", roll10, "Total:", total10)

## Output:
# Rolled 6-sided die: 1 Total: 1
# Rolled 10-sided die: 4 Total: 4
# Rolled 10-sided die: 9 Total: 13

Timer Replacement #

A simple timer class can easily be created using closures.

import time

def start_timer():
    start_time_ms = time.time_ns() // 1_000_000

    def elapsed():
        return (time.time_ns() // 1_000_000) - start_time_ms

    return elapsed


process_elapsed_time = start_timer()
time.sleep(1)
print("Elapsed time:", process_elapsed_time(), "ms")
time.sleep(1)
print("Elapsed time:", process_elapsed_time(), "ms")

## Output:
# Elapsed time: 1000 ms
# Elapsed time: 2000 ms

Conclusion #

Closures are a powerful feature of Python that allow you to create quick mini classes without the need for a full class definition.

Less boilerplate code means less room for errors and easier maintenance.