2021-11-24T18:13:37Z

Using JavaScript-style async promises in Python

When you compare Python and JavaScript async code, you may think they are very much alike, since both use the async/await pattern. In reality, there is a significant difference between the two, which in my opinion, makes the JavaScript async model superior to Python's.

Do you want to know what the difference is, and how to make async Python work like async JavaScript? Watch or read on for the details!

How Async Works in Python

Let's run a quick experiment in the Python shell. Instead of starting the shell by just typing python, use the following command, which allows you to use await directly in the Python prompt:

python -m asyncio

Let's write a quick async function to have something to test with:

async def f():
    print('f is running')
    return 42

The function has a print statement, so that we can see in the terminal when it runs. To keep things simple, this function just returns a number. A longer function that performs asynchronous tasks would work just as well, so feel free to make the function more complex with some await statements before the return value if you like.

Let's run the function and get its result:

>>> result = f()
>>> result
<coroutine object f at 0x109a9f1c0>

Interesting, right? Calling the function doesn't really run it. If the function had executed, we would see the printed text in the terminal, so we know that the function did not run. You can see that calling the async function just returned a coroutine object without actually running the function. The coroutine object is now stored in the result variable.

So how can we get the function to run? We have to "await" the coroutine:

>>> await result
f is running
42

In my opinion the way this works in Python is extremely counterintuitive. This is the main aspect of the Python async model that I dislike.

In practice, you will find that the function invocation is combined with the await, all in a single statement, which somewhat hides the odd behavior we just experienced:

>>> await f()
f is running
42

But this usage has a problem that bites me often. I frequently forget to add the await keyword before the function invocation, and when this happens the function doesn't run, but execution continues without any errors, because calling the function just to get a coroutine is a perfectly legal thing to do. To be fair, Python does print a warning when you fail to await a coroutine, but this happens when the process exits.

How Async Works in JavaScript

Should we repeat the experiment in JavaScript to learn about the differences in async implementations?

Here is how to start node with support for awaiting in the REPL prompt:

node --experimental-repl-await

Below you can see a similar f() function, with a print statement and a return of a number:

const f = async () => {
  console.log('f is running');
  return 42;
};

Let's call this function and see what happens:

> let result = f();
f is running
undefined

So this is interesting! In JavaScript, when you call an async function, the function executes, as you would expect. Note that the undefined is the return value of the let statement, not the value that was assigned to the result variable.

Let's take a look at result:

> result
Promise {
  42,
  [Symbol(async_id_symbol)]: 357,
  [Symbol(trigger_async_id_symbol)]: 5,
  [Symbol(destroyed)]: { destroyed: false }
}

So our result variable received a Promise, which is a core building block of the JavaScript async model.

In JavaScript, an async function is defined as a function that returns a promise. You can write it as a standard function (i.e. without the async keyword) and return the Promise object explicitly, or if you prefer, you can use the async qualifier and then the JavaScript compiler creates and returns the promise for you.

Unlike in Python, async JavaScript functions are normal functions. If you call them, they execute. This is a huge advantage, as it avoids having to treat async functions differently than regular ones.

So what is a promise, anyway? A promise is an object that is associated with an asynchronous task. An asynchronous function is supposed to launch the background work and immediately return a promise, without waiting for this work to complete. The caller can then use the promise object to determine when the work has completed.

How do you get the result from a promise? There are a couple of ways, but the most convenient is to await the promise:

> await result;
42

So here is what makes a lot of people think that JavaScript and Python have the same async solution. When you combine calling the function with the await, both languages look exactly the same. Check out how the JavaScript async function is called in a single line:

> await f();
42

This is identical to the Python solution. But of course, if you forget the await in JavaScript, the function still runs. You will get some concurrency issues because the function will run in the background while the calling code continues to run concurrently, but at least the function will execute, which in my view is a much better outcome.

The Promise class in JavaScript is extremely powerful. It provides lots of interesting ways to work with concurrency beyond async/await. For example, its then() method makes it possible to create chains of functions that execute asynchronously. Below you can see how to attach a callback (the console.log function) to our promise. JavaScript will run the provided function, with the result of the asynchronous function passed as an argument.

> result.then(console.log);
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 636,
  [Symbol(trigger_async_id_symbol)]: 357,
  [Symbol(destroyed)]: { destroyed: false }
}
42

Making Python Async More Like JavaScript Async

I always wondered if it would be possible to make Python use a promise-based async solution. The other day I spent a few hours trying to replicate promises in Python, and the result is a new package that I called promisio. You can install it with pip:

pip install promisio

Promisio includes a JavaScript-compatible Promise class, but most importantly, it comes with a promisify decorator that can convert any function (async or not) to a promise-based function that works as in JavaScript:

from promisio import promisify

@promisify
async def f():
    print('f is running')
    return 42

A function decorated with promisify can be called directly, and it runs, returning a promise that can be awaited to get the result:

>>> result = f()
f is running
>>> result
<promisio.TaskPromise object at 0x10bc5ad60>
>>> await result
42

The decorator also works on regular functions, because as in JavaScript, the only requirement is that the function returns a promise. There is no requirement that an asynchronous function is defined with the async keyword:

@promisify
def g(x):
    print('g is running')
    return x * 2

Regardless of the original function being async or not, the resulting promise-based function is always async and must be awaited to get a result:

>>> await g(21)
g is running
42

The Promise class that I implemented in the promisio package matches the JavaScript one in functionality, so all the concurrency features available in JavaScript can also be used in Python. It is especially interesting that these features do not require the use of async and await, opening the door to using async features in regular functions, something that is quite difficult in Python. Here is how to use the then() method to configure a completion callback that prints the result once it becomes available:

>>> result = g(21)
g is running
>>> result.then(print)
<promisio.Promise object at 0x10bc76280>
42

Conclusion

I'm quite happy with the promise implementation I made for Python, and I hope you decide to try it out. Please let me know what you think below in the comments!

3 comments

  • #1 Jose Laurentino III said 2021-11-25T07:06:39Z

    Hello Miguel:

            Very interesting work. Thanks for taking the time to provide the information and details.
    
            Oh PHP/JavaScript I over-used promise for images throughout my entire application. That gave me the security of not allowing direct access to any images in the application. On the top of that, it also helped as a way to block hackers by running their patience out with so much bits-n-bytes.
    
            I just got involved with Python and want to fast forward. I just learned a bit about Python-with-ajax (from Django Central) and believe that your library--promisio--will be of great value at some point as I learn more.
    

    Thank you and happy thanksgiving!

    Laurentino

  • #2 Jacob said 2021-11-26T22:58:08Z

    Just a minor correction: Promises in JS don't run in parallel. They run out of order (outside of the synchronous flow (hence asynchronous). All that means is that the promise won't necessarily be evaluated in the order in which it was declared. The only way to achieve true parallelism in JS is with the platform's implementation of a worker thread.

    Just thought I'd mention it

  • #3 Miguel Grinberg said 2021-11-26T23:35:24Z

    @Jacob: yes, but to be correct, promises don't run at all, parallel or not. A promise is just an object that holds the state of a task. Tasks are what runs. I have edited out the reference to running in parallel, since you are correct in that this word is used to describe a different type of concurrency that is not what this article is about. What I meant to say is that the async task will run at the same time as the calling task. This is true for both languages.

Leave a Comment