How to Get Asyncio Task Results
A coroutine may return a result directly via a return value.
Asyncio tasks that execute a coroutine run asynchronously. Therefore we need a way to retrieve results from coroutines executed by independently run coroutines.
In this tutorial, you will discover how to get a result from an asyncio task.
After completing this tutorial, you will know:
- How to retrieve a result from an asyncio task.
- What happens to the result if the task fails or is cancelled.
- What happens if we try to retrieve a result from a running task.
Let's get started.
What is an Asyncio Task
An asyncio Task is an object that schedules and independently runs an asyncio coroutine.
It provides a handle on a scheduled coroutine that an asyncio program can query and use to interact with the coroutine.
A Task is an object that manages an independently running coroutine.
-- PEP 3156 – Asynchronous IO Support Rebooted: the "asyncio" Module
An asyncio task is represented via an instance of the asyncio.Task class.
A task is created from a coroutine. It requires a coroutine object, wraps the coroutine, schedules it for execution, and provides ways to interact with it.
A task is executed independently. This means it is scheduled in the asyncio event loop and will execute regardless of what else happens in the coroutine that created it. This is different from executing a coroutine directly, where the caller must wait for it to complete.
Tasks are used to schedule coroutines concurrently. When a coroutine is wrapped into a Task with functions like asyncio.create_task() the coroutine is automatically scheduled to run soon
-- Coroutines and Tasks
We can create a task using the asyncio.create_task() function.
This function takes a coroutine instance and an optional name for the task and returns an asyncio.Task instance.
Wrap the coro coroutine into a Task and schedule its execution. Return the Task object.
-- Coroutines and Tasks
For example:
...
# create and schedule a task
task = asyncio.create_task(coro)
You can learn more about asyncio tasks in the tutorial:
Now that we know about asyncio tasks, let's look at how we might get results from tasks.
How to Get Task Result
We can get the result of a task via the result() method.
This method returns the return value of the coroutine wrapped by the Task or None if the wrapped coroutine does not explicitly return a value.
For example:
...
# get the return value from the wrapped coroutine
value = task.result()
If the coroutine fails with an unhandled exception, it is re-raised when calling the result() method and may need to be handled.
For example:
...
try:
# get the return value from the wrapped coroutine
value = task.result()
except Exception:
# task failed and there is no result
If the task was canceled, then a CancelledError exception is raised when calling the result() method and may need to be handled.
For example:
...
try:
# get the return value from the wrapped coroutine
value = task.result()
except asyncio.CancelledError:
# task was canceled
As such, it is a good idea to check if the task was canceled first.
For example:
...
# check if the task was not canceled
if not task.cancelled():
# get the return value from the wrapped coroutine
value = task.result()
else:
# task was canceled
If the task is not yet done, then an InvalidStateError exception is raised when calling the result() method and may need to be handled.
For example:
...
try:
# get the return value from the wrapped coroutine
value = task.result()
except asyncio.InvalidStateError:
# task is not yet done
As such, it is a good idea to check if the task is done first.
For example:
...
# check if the task is not done
if not task.done():
await task
# get the return value from the wrapped coroutine
value = task.result()
Now that we know how to get the return value from a task, let's look at some worked examples.
Example of Getting a Result From a Done Task
We can explore how to get a return value result from a successfully done task.
In this example, we define a task coroutine that reports a message, blocks for a moment, then returns a value.
We then define the main coroutine that is used as the entry point into the asyncio program. It reports a message, creates and schedules the task, then awaits the task to be completed. Once completed, it retrieves the result from the task and reports it.
The complete example is listed below.
# SuperFastPython.com
# example of getting a result from a done task
import asyncio
# define a coroutine for a task
async def task_coroutine():
# report a message
print('executing the task')
# block for a moment
await asyncio.sleep(1)
# return a value
return 99
# custom coroutine
async def main():
# report a message
print('main coroutine started')
# create and schedule the task
task = asyncio.create_task(task_coroutine())
# wait for the task to complete
await task
# get the result
value = task.result()
print(f'result: {value}')
# report a final message
print('main coroutine done')
# start the asyncio program
asyncio.run(main())
Running the example starts the asyncio event loop and executes the main() coroutine.
The main() coroutine reports a message, then creates and schedules the task coroutine.
It then suspends and awaits the task to be completed.
The task runs, reports a message, and sleeps for a moment before returning a value and terminating normally.
The main() coroutine resumes and retrieves the return value result from the task, which is then reported.
This example highlights the normal case of retrieving a return value from a successful task.
main coroutine started
executing the task
result: 99
main coroutine done
Next, we will look at getting a result from a task that does not return a value.
Example of Getting a Result From a Task That Does Not Return a Value
If the coroutine that the task wraps does not return a value, then the result() method returns the default return value None.
We can explore getting a result from a task that does not return a value.
In the example below, we update the example from the previous section so that the task coroutine does not return a value.
The complete example is listed below.
# SuperFastPython.com
# example of getting a result from a done task that does not return a value
import asyncio
# define a coroutine for a task
async def task_coroutine():
# report a message
print('executing the task')
# block for a moment
await asyncio.sleep(1)
# custom coroutine
async def main():
# report a message
print('main coroutine started')
# create and schedule the task
task = asyncio.create_task(task_coroutine())
# wait for the task to complete
await task
# get the result
value = task.result()
print(f'result: {value}')
# report a final message
print('main coroutine done')
# start the asyncio program
asyncio.run(main())
Running the example starts the asyncio event loop and executes the main() coroutine.
The main() coroutine reports a message, then creates and schedules the task coroutine.
It then suspends and awaits the task to be completed.
The task runs, reports a message, and sleeps for a moment before terminating normally without returning a value.
The main() coroutine resumes and attempts to retrieve a return value result from the task, which is then reported.
We can see that the return value for the task is None because it did not explicitly return a value.
This example highlights that we can retrieve a result from a task that does not return a value and expect to receive the value None.
main coroutine started
executing the task
result: None
main coroutine done
Example of Getting a Result From a Failed Task
If the task fails with an unhandled exception, the exception will be re-raised when calling the result() method on the task to get the result.
As such, we may need to handle possible exceptions when getting task results.
We can explore getting a result from a task that failed with an unhandled exception.
In this example, we can update the task coroutine to explicitly raise an exception that is not handled.
This will cause the task's coroutine to fail.
The main coroutine will sleep to wait for the task to be completed. This is to avoid using the await expression which will also propagate the exception back to the caller.
You can learn more about handling exceptions in tasks in the tutorial:
Once the task is done, the main coroutine will attempt to retrieve the return value and handle the exception that is re-raised.
The complete example is listed below.
# SuperFastPython.com
# example of getting a result from an asyncio task that failed
import asyncio
# define a coroutine for a task
async def task_coroutine():
# report a message
print('executing the task')
# block for a moment
await asyncio.sleep(1)
# fail with an exception
raise Exception('Something bad happened')
# return a value (never reached)
return 99
# custom coroutine
async def main():
# report a message
print('main coroutine started')
# create and schedule the task
task = asyncio.create_task(task_coroutine())
# wait for the task to complete
await asyncio.sleep(1.1)
try:
# get the result
value = task.result()
print(f'result: {value}')
except Exception as e:
print(f'Failed with: {e}')
# report a final message
print('main coroutine done')
# start the asyncio program
asyncio.run(main())
Running the example starts the asyncio event loop and executes the main() coroutine.
The main() coroutine reports a message, then creates and schedules the task coroutine.
It then suspends and awaits the task to be completed.
The task runs, reports a message, and sleeps for a moment. The task resumes and raises an exception.
The exception does not terminate the application or the asyncio event loop.
Instead, the exception is captured by the asyncio event loop and stored in the task.
The main() coroutine resumes and then attempts to retrieve the result from the task. This fails and the exception that was raised and not handled in the Task's wrapped coroutine is re-raised in the caller.
The main() coroutine catches the exception and reports its details.
main coroutine started
executing the task
Failed with: Something bad happened
main coroutine done
Next, let's look at what happens if we try to get a task result from a running task.
Example of Getting a Result From a Running Task
We cannot retrieve results from a running asyncio task.
Instead, we can only retrieve a result from a task after it is done.
If we call the result() method on a task that is scheduled or running, an InvalidStateError exception is raised by the caller.
In the example below, we update the above example so that the main coroutine schedules the task, waits a moment then attempts to get the result too soon before the task has been completed.
This is expected to raise an InvalidStateError.
The complete example is listed below.
# SuperFastPython.com
# example of getting a result from a running task
import asyncio
# define a coroutine for a task
async def task_coroutine():
# report a message
print('executing the task')
# block for a moment
await asyncio.sleep(1)
# return a value (never reached)
return 99
# custom coroutine
async def main():
# report a message
print('main coroutine started')
# create and schedule the task
task = asyncio.create_task(task_coroutine())
# wait for the task to complete
await asyncio.sleep(0.1)
# get the result
value = task.result()
print(f'result: {value}')
# report a final message
print('main coroutine done')
# start the asyncio program
asyncio.run(main())
Running the example starts the asyncio event loop and executes the main() coroutine.
The main() coroutine reports a message, then creates and schedules the task coroutine.
It then suspends and sleeps for a moment.
The task runs, reports a message, and sleeps for a moment.
The main() coroutine resumes and attempts to retrieve the result from the task while the task is running, even though the task is suspended.
This fails with an InvalidStateError that breaks the asyncio event loop in this case.
This example highlights that we must always retrieve a Task result after the task is done.
main coroutine started
executing the task
Traceback (most recent call last):
...
asyncio.exceptions.InvalidStateError: Result is not set.
We can check if a task is done before retrieving the exception via the done() method that will return True if the task is done, or False otherwise.
Next, we can look at the case of attempting to get a task result from a canceled task.
Example of Getting a Result From a Canceled Task
We cannot retrieve a result from a canceled task.
Although a canceled task is done, a result will not be available and cannot be retrieved.
Instead, a CancelledError exception is raised when calling the result() method if the task was canceled.
The example below updates the previous example to create and schedule the task as before, then wait a moment. It then cancels the task, waits a moment for the task to be canceled, then attempts to get the result.
This is expected to fail as the result() method will re-raise the CancelledError exception from the wrapped coroutine that was used to cancel the task.
The complete example is listed below.
# SuperFastPython.com
# example of getting a result from a canceled task
import asyncio
# define a coroutine for a task
async def task_coroutine():
# report a message
print('executing the task')
# block for a moment
await asyncio.sleep(1)
# return a value (never reached)
return 99
# custom coroutine
async def main():
# report a message
print('main coroutine started')
# create and schedule the task
task = asyncio.create_task(task_coroutine())
# wait for the task to complete
await asyncio.sleep(0.1)
# cancel the task
task.cancel()
# wait a moment for the task to be canceled
await asyncio.sleep(0.1)
# get the result
value = task.result()
print(f'result: {value}')
# report a final message
print('main coroutine done')
# start the asyncio program
asyncio.run(main())
Running the example starts the asyncio event loop and executes the main() coroutine.
The main() coroutine reports a message, then creates and schedules the task coroutine.
It then suspends and sleeps for a moment.
The main() coroutine resumes and cancels the task. It then suspends and waits a moment for the task to respond to the request for being canceled.
The task is canceled by raising a CancelledError within the wrapped coroutine.
The main() coroutine resumes and attempts to retrieve the result.
This fails and the CancelledError exception is re-raised in the caller.
This breaks the event loop in this case.
main coroutine started
executing the task
Traceback (most recent call last):
...
asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
asyncio.exceptions.CancelledError
Takeaways
You now know how to get a result from an asyncio task in Python.
If you enjoyed this tutorial, you will love my book: Python Asyncio Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.