How to Implement Series of Promises in Python

Abdul Mateen Feb 02, 2024
  1. Asynchronous Programming in Python
  2. Callback Function
  3. try/except in Python
  4. Series of Promises in Python
How to Implement Series of Promises in Python

This tutorial will teach us how to write a series of promises in Python. First, we will discuss asynchronous programming in Python.

Next, we will discuss the callback function in Python. Finally, before coming to the actual topic, we will briefly discuss try/except in Python and then come to a series of promises in Python.

Asynchronous Programming in Python

This article expects you to have a basic idea of operating system threads. If you don’t have a preliminary introduction to threads, you can read threads in operating systems as a prerequisite.

Asynchronous programming allows multiple threads to run in parallel, whereas the main program (normally called the main/manager thread) can create multiple worker threads. Typically, main threads wait for worker threads, which inform the main thread after completing the task.

Unlike regular programming, instead of holding control until the competition, the asynchronous function pauses and allows the running of other functions (threads) in parallel.

We will discuss and give an example of asynchronous programming in Python; however, better first to see a related synchronous code. This code will help develop understanding by comparison.

def count():
    for i in range(5):
        print(i, end=" ")


def main():
    count()
    count()
    count()


main()

Here, we call the count function three times in sequence. The output is as per expectations.

0 1 2 3 4 0 1 2 3 4 0 1 2 3 4

You can see the output of the first count function, followed by the output of the second call of the count function, followed by the output of the last count function.

The asyncio library of Python allows running asynchronous programs in Python. The first requirement for asynchronous programming is to design functions as awaitable objects.

There are two requirements to convert a standard function to awaitable object. The first is using the async keyword (before the def keyword) to create asynchronous functions instead of routine functions.

The second requirement is to call the sleep function inside asynchronous functions, suspend the current function, and control other functions.

The sleep statement is the specific point in the code where the function exactly goes to suspend state. The second requirement of asynchronous programming is to add await while calling awaitable objects (asynchronous functions); otherwise, there will be an error.

The await keyword tells the event loop to suspend the current function to give running time to other functions.

The third requirement is to call the gather function and pass awaitable objects (asynchronous functions). The gather function runs these functions in their order but concurrently.

It means that the first function starts at first and, after some time, the second function also starts in parallel. Similarly, all asynchronous functions start running concurrently, one by one.

Now, let’s see the code.

import asyncio


async def count():
    for i in range(5):
        print(i, end=" ")
        await asyncio.sleep(0.5)


async def main():
    await asyncio.gather(count(), count(), count())


if __name__ == "__main__":

    asyncio.run(main())

Here, we have converted our previous code to asynchronous code with certain additions. In the first line, the asyncio library is imported.

The async keyword is added at the start of all the functions.

The sleep function call is added to the count function. The await keyword is added with all the function calls, including the main function.

Lastly, the gather function is called in the main, where the count function is called multiple times to demonstrate each function call as a separate thread.

Using the gather function, we add awaitable objects to form a group of asynchronous functions to run concurrently. Let’s see the output of this code.

0 0 0 1 1 1 2 2 2 3 3 3 4 4 4

In the output, you can see that all threads run in parallel and produce asynchronous output instead of running altogether.

You may confuse with calling the same function multiple times; here is another example where we have different functions running in parallel.

import asyncio


async def count1():
    for i in range(10):
        print(i, end=" ")
        await asyncio.sleep(0.5)


async def count2():
    for i in range(50, 60):
        print(i, end=" ")
        await asyncio.sleep(0.5)


async def main():
    await asyncio.gather(count1(), count2())


asyncio.run(main())

The output of this code is:

0 50 1 51 2 52 3 53 4 54 5 55 6 56 7 57 8 58 9 59

Again, both functions are running concurrently.

Callback Function

A callback is passed to another function (as an argument). The other function is supposed to callback this function somewhere in its definition.

However, the calling point is depended on how another function is defined.

Here, we have a straightforward coding example related to the callback function.

import random as r


def callback1(s):
    print(f"******* {s} *******")


def callback2(s):
    print(f"^^^^^^^ {s} ^^^^^^^")


def print_through_callback(message, f1, f2):
    if r.randint(0, 1) == 0:
        f1(message)
    else:
        f2(message)


def main():

    print_through_callback("Callback Example", callback1, callback2)


main()

In this code, our print function has three parameters. The second and third parameters are some function names.

In the main, we pass two functions, and the code randomly calls one. If you run this code multiple times, you can see both functions are called randomly.

try/except in Python

Python also offers exception handling. In Python, we have a try block to test the code; it has the potential for an exception, and in the except block, you can handle the exception.

We all know that dividing by zero is not defined, and programs (in almost every programming language) crash; when we call the divide by zero operation. If you have no idea, then try this code.

def main():
    x = int(input("Enter any number:"))
    print(2 / x)


main()

Input zero and see the result; your program will crash. Crash of code is a bad thing and should be avoided through exception handling.

See the same code with exception handling.

def main():
    try:
        x = int(input("Enter any number:"))
        print(2 / x)
    except:
        print("Divide by zero is not defined")


main()

You should run this code and input non-zero values; you will get the result of the division operation, written inside print (2/x); if you enter zero, the program will give the message Divide by zero is not defined instead of a crash.

Series of Promises in Python

Callback functions are the same as normal functions; however, their use differs.

Consider heavy-weight functions that take a lot of time to execute. Usually, such functions are made asynchronous.

Asynchronous functions will execute in the background, and after a specific time, they will complete, whereas other functions will start in parallel. However, if you want to run some function after completing some heavy-weight function, the choice is to use the callback function.

However, there is an issue with the completion of such tasks. What if the task throws an exception before completion?

To ensure that the function is called after successful completion of the task, there is a need for promises and asynchronous programming.

Promises

A promise is an object that represents the successful or unsuccessful (failure) completion of an asynchronous function.

Promise objects also represent the resultant value from the asynchronous function. A promise is used to manage the issues related to multiple callbacks.

You may use promise API to execute a series of promises in Python. However, we can achieve the same purpose in Python through async/await.

For implementation in Python with asynchronous functions, we have to use the asyncio library with asynchronous functions. We may call functions in sequence with the await keyword, which is already described above.

Finally, we are using the try/except block. First, see the code and the output.

Later, we will explain the purpose of the try/except block.

import asyncio
import random as r


async def f1(x):
    await asyncio.sleep(1)
    return x ** 2


async def f2(x):
    await asyncio.sleep(1)
    return x / 2


async def f3(x):
    if r.randint(0, 1) == 0:
        return x
    raise ValueError(x)


async def run():
    try:
        value = await f3(await f2(await f1(r.randint(5, 9))))
    except ValueError as exception:
        print("Exception Occurred:", exception.args[0])
    else:
        print("No Exception:", value)


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run())
    loop.close()

The following is the combined output for the above Python script over 5 runs.

No Exception: 12.5
Exception Occurred: 12.5
Exception Occurred: 32.0
No Exception: 18.0
No Exception: 40.5

The output is just describing that we may have success or failure in some asynchronous operation. Therefore, we can place a statement for the callback function after the function call in the try block.

In case of successful completion, the code will execute the callback function. In failure, the control will go to the except block and ignore the callback function.

In this way, we can handle the series of promises in Python; if we have successful completion, then execute the callback function (which depends on the successful completion of some required task); otherwise, we don’t have to execute our callback function.