I didn't have a great grasp of async objects in Python until I'd worked with Promises in Javascript. If you're struggling too, maybe this approach will help you. Let's walk through a basic example of a Promise in Javascript and then show how that translates to async in Python.

The Javascript Promise

A Promise is a "proxy for a value that will eventually become available." I think of it as an operation that has already started but hasn't necessarily completed yet. When incomplete, the Promise is considered to be in a "pending" state. When completed, the Promise progresses to the "resolved" state.

For example, the Fetch API returns a Promise to retrieve data from a URL:

const url = "https://example.com";
fetch(url)
    .then(response => console.log(response))
    .catch(error => console.log(error));
console.log(url);

When this code snippet runs, fetch() is called, but the data from the URL takes time to retrieve. This does not pause program execution! The fetch() call returns a Promise as a placeholder for the future data, and the engine continues to the next block of code, the line console.log(url). Eventually, the URL data is fetched, at which point the Promise state transitions from pending to resolved and the code inside the then block is run. If any errors occurred while fetching data, the catch block is run instead.

A key insight here is that the line-by-line order in which code is written is no longer the order in which it's executed. Depending on how quickly it takes the fetch() Promise to resolve, it's possible for the console.log(response) to happen either before or after console.log(url).

The Python Awaitable

Python has many async objects that inherit the Awaitable interface, but the three main ones are coroutines, Tasks, and Futures. Of these, the Task is most similar to the Javascript Promise.

Let's take the same Javascript snippet from before and translate it to Python:

from asyncio import create_task


async def fetch(url):
	# Fetch data asynchronously from a URL 
    pass


url = "https://example.com"
task = create_task(fetch(url))
print(url)
try:
    response = await task
    print(response)
except Exception as exc:
    print(exc)

Here, the create_task() call quickly returns an Awaitable object that serves as a placeholder for the fetched data and allows code execution to immediately proceed to the print(url) line below. However, unlike the Javascript Promise, there is no then or catch chaining to process the output once the operation is complete. Instead, the Task must be explicitly awaited when the output is needed.

If the operation hasn't completed by the time the await task is called, the program halts until the data is available. At this point, the program is "blocked." It may seem like CPU cycles are wasted while waiting, but under the hood, await will yield control back to the event loop and allow code elsewhere to run.

This is an important distinction between the Python and Javascript implementations that bears repeating. In Javascript, the Promise triggers a then or catch block when it resolves. In Python, the Task triggers nothing and we have to explicitly await the output. This means that the order in which the output is printed in the Python implementation is always the same! Even when the data is fetched instantaneously, the print(url) line always occurs first.

Which is better?

I can't fairly claim one async interface is strictly better, but there have been times when I've preferred one over the other.

Most of the time, I appreciate how the Promise interface encourages all related logic to be close together. It's easier for me to see all relevant code at the same time, to build a mental map of the program's architecture, and to remember to handle exceptions with the dedicated catch block. With Python, I sometimes struggle to find where a Task's output is used (if ever) once it's been created, and async exceptions often go unnoticed, which can make debugging difficult.

On the other hand, it's convenient to read a Python program linearly and confidently assume that the code will be more-or-less executed in that order. I can effortlessly trace the code path and predict when and where exceptions are likely to occur. Debugging Javascript isn't as smooth.

Of course, I've only touched on a very small portion of the Promise and Awaitable interfaces. Each comes with its own set of great features that would take a very long blog post to discuss thoroughly. Until that post is written, I hope this comparison of the Javascript Promise and Python Task helped shed some light on async programming in Python.