Python: Defining a class with an async constructor (3 ways)

Updated: August 1, 2023 By: Wolf Post a comment

When working with modern Python, there might be cases where you want to create an async constructor for a class, such as:

  • You want to initialize some attributes of the class with the result of an asynchronous operation, such as fetching data from a web service, reading a file, or connecting to a database.
  • You want to perform some validation or computation on the input arguments of the class that requires awaiting a coroutine.
  • You want to register the instance of the class as an event listener or a callback handler for an async event loop.

Unfortunately, Python doesn’t support async constructors natively, so you have to use some alternative approaches to achieve the same effect. This succinct, example-based article will walk you through 3 different ways to define a class with an async constructor. Without any further ado, let’s get started!

Using a factory method

The core idea of this solution is to use a class method that is marked as async to create and initialize an instance of the class asynchronously. The constructor of the class is kept simple and does not use await. The detailed steps to implement the idea are listed below:

  1. Define a class with a simple constructor that takes the necessary arguments and assigns them to the instance attributes.
  2. Define a class method that is marked as async and takes the same arguments as the constructor. This method will create an instance of the class using the constructor and then perform any asynchronous initialization tasks using await. The method will return the initialized instance.
  3. To create an instance of the class, use await with the factory method instead of the constructor.

Example:

# SlingAcademy.com
# This code uses Python 3.11.4

import asyncio


# A class that represents a timer
class Timer:
    # A simple constructor that takes the duration
    def __init__(self, duration):
        self.duration = duration
        self.start_time = None
        self.end_time = None

    # A factory method that creates and initializes an instance asynchronously
    @classmethod
    async def create(cls, duration):
        # Create an instance using the constructor
        self = Timer(duration)

        # Simulate getting the current time
        self.start_time = (
            asyncio.get_running_loop().time()
        )  

        # Simulate waiting for the duration
        await asyncio.sleep(duration)  

        # Simulate getting the current time
        self.end_time = (
            asyncio.get_running_loop().time()
        )  
        return self


# To create an instance of the class, use await with the factory method
async def main():
    timer = await Timer.create(5)
    print(f"The timer ran for {timer.end_time - timer.start_time} seconds")


asyncio.run(main())

Output:

The timer ran for 5.001732666998578 seconds

Note that this approach requires an extra method call to create an instance of the class. The constructor cannot be used directly with await.

Using a coroutine function

This approach uses a top-level coroutine function that creates and initializes an instance of the class asynchronously. The constructor of the class is kept simple and does not use await.

This approach is similar to the factory method approach, but it does not require defining a class method. It can be useful when the initialization logic is complex or depends on external factors.

The steps:

  1. Define a class with a simple constructor that takes the necessary arguments and assigns them to the instance attributes.
  2. Define a coroutine function that takes the same arguments as the constructor. This function will create an instance of the class using the constructor and then perform any asynchronous initialization tasks using await. The function will return the initialized instance.
  3. To create an instance of the class, use await with the coroutine function instead of the constructor.

In this example, we’ll define a class called Dice that represents a dice. This class has a method named roll_dice() to simulate the act of rolling the dice:

# SlingAcademy.com
# This code uses Python 3.11.4

import asyncio
import random


# A class that represents a dice
class Dice:
    # A simple constructor that takes the number of sides
    def __init__(self, sides):
        self.sides = sides
        self.value = None


# A coroutine function that creates and initializes an instance asynchronously
async def roll_dice(sides):
    # Create an instance using the constructor
    dice = Dice(sides)
    # Perform any asynchronous initialization tasks using await
    dice.value = await asyncio.sleep(
        1, random.randint(1, sides)
    )  # Simulate rolling the dice
    return dice


# To create an instance of the class, use await with the coroutine function
async def main():
    dice = await roll_dice(6)
    print(f"The dice rolled {dice.value}")


asyncio.run(main())

Output (may vary, due to the randomness):

The dice rolled 4

The downside of this approach is that it requires defining a separate function for each class that needs an async constructor. The function name may not be consistent with the class name. The constructor cannot be used directly with await.

Using the __await__() magic method

The strategy here is to utilize the __await__() magic method to make the class itself awaitable. The constructor of the class can use await to perform any asynchronous initialization tasks. However, this approach requires some tricks to make it work. The code will be complicated, and may not work in all cases.

The advantage of using the __await__() magic method is that it allows using the constructor directly with await, without needing an extra method or function call. It can be useful when the initialization logic is simple and does not depend on external factors.

The steps:

  1. Define a class with a constructor that takes the necessary arguments and assigns them to the instance attributes.
  2. Define an __await__() magic method that returns an iterator over a coroutine object. The coroutine object can be either the constructor itself or another method that calls the constructor and returns the instance.
  3. To create an instance of the class, use await with the class name instead of the constructor.

A code example is worth more than a thousand words:

# SlingAcademy.com
# This code uses Python 3.11.4

import asyncio


class AsyncClass:
    # The constructor of the class
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # Use the __await__ method to make the class awaitable
    def __await__(self):
        # Call ls the constructor and returns the instance
        return self.create().__await__()

    # A method that creates an instance of the class asynchronously
    async def create(self):
        # Perform some asynchronous initialization tasks
        await asyncio.sleep(1)
        print(
            f"Creating an instance of {self.__class__.__name__} with name {self.name} and age {self.age}"
        )
        # Perform some other asynchronous initialization tasks
        await asyncio.sleep(2)
        # Return the instance
        return self


async def main():
    # Create an instance of AsyncClass with "await"
    alice = await AsyncClass("Mr. Wolf", 99)
    # Print the instance attributes
    print(f"name: {alice.name}, age: {alice.age}")


# Run the main function
asyncio.run(main())

Output (with a delay before each print):

Creating an instance of AsyncClass with name Mr. Wolf and age 99
name: Mr. Wolf, age: 99

Afterword

We’ve gone over 3 different workarounds to implement async constructor-likes for Python classes. Depending on your needs, choose one of them to go with. This tutorial ends here. Happy coding & have fun with programming!