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!
#1 Jose Laurentino III said 2021-11-25T07:06:39Z
#2 Jacob said 2021-11-26T22:58:08Z
#3 Miguel Grinberg said 2021-11-26T23:35:24Z
#4 RyomaHan said 2021-12-22T03:17:42Z
#5 Duncan Booth said 2022-01-17T15:50:04Z
#6 Miguel Grinberg said 2022-01-17T18:33:05Z
#7 adatron said 2022-02-15T05:05:21Z
#8 Miguel Grinberg said 2022-02-15T10:59:21Z
#9 adatron said 2022-02-15T12:26:01Z
#10 Sabine said 2022-04-22T14:36:14Z