Last Updated on August 18, 2023
The Python standard library provides two Future classes.
The first is in the concurrent.futures module and the second is in the asyncio module:
- concurrent.futures.Future
- asyncio.Future
This raises the question:
What is the difference between the concurrent.futures.Future and asyncio.Future classes?
Are they compatible with each other?
For example, can we use asyncio.Future instances in the concurrent.futures.wait() and concurrent.futures.as_completed() functions or concurrent.futures.Future instances in the asyncio.gather() function?
How are the classes similar and how are they different?
In this tutorial, you will discover the similarities and differences between the concurrent.futures.Future and asyncio.Future classes in Python.
Let’s get started.
What is concurrent.futures.Future
The concurrent.futures.Future is a class that is part of the Executor framework for concurrency in Python.
It is used to represent a task executed asynchronously in the ThreadPoolExecutor and ProcessPoolExecutor classes.
The Future class encapsulates the asynchronous execution of a callable. Future instances are created by Executor.submit().
— concurrent.futures — Launching parallel tasks
We do not create a Future object, instead, a Future object is created for us for an issued task.
A function can be issued to a thread pool or process pool for asynchronous execution using the submit() method, which returns a Future instance.
For example:
1 2 3 |
... # issue task future = exe.submit(task) |
The Future object provides a handle on the task executing in the Executor, providing a suite of capabilities, such as:
- Checking the status of the task, such as whether it is done, running, or canceled.
- Getting results from the task, e.g. return values, once the task is complete.
- Add timeouts when waiting for the task to complete.
- Providing access to exceptions raised in the task while running.
- Adding callback functions to execute automatically once the task is done.
Additionally, the concurrent.futures module provides utility functions for working with collections of Future objects as a group, such as the concurrent.futures.wait() and concurrent.futures.as_completed() functions.
You can learn more about concurrent.futures.Future object when used with the ThreadPoolExecutor in the tutorial:
You can learn more about concurrent.futures.Future object when used with the ProcessPoolExecutor in the tutorial:
Let’s take a closer look at each of these use cases.
Future Object Status
We can check on the status of our asynchronous task via its Future object.
For example, we may want to check on the status of our task, such as whether it is currently running, is done, or perhaps has been canceled.
This can be achieved by calling methods on the Future object; for example:
1 2 3 4 |
... # check if the task is running if future.running(): # do something... |
The three methods we can use to check the status of our task are running(), done(), and cancelled().
- running() Returns True if the task is currently running, False otherwise.
- done() Returns True if the task has been completed, False otherwise.
- cancelled() Returns True if the task was canceled, False otherwise.
You can learn more about checking the status of a Future object in the tutorials:
- How to Check Task Status in the ThreadPoolExecutor
- How to Check the Status of a Task in a ProcessPoolExecutor
Future Object Results
We can also use the Future objects to get the result from the task or the exception if one was raised during the execution of the task.
This can be achieved by calling the result() method for the result and the exception() method to retrieve the exception.
- result() Returns the result of the target task function once complete, or None if the function does not return a value.
- exception() Returns the exception raised during the execution of the task once complete, or None if no exception was raised.
The result() and exception() methods are blocking, meaning that they only return once the task has been completed, e.g. done() returns True.
This means that the calls to result() and exception() will wait and not return until the task is completed.
1 2 3 |
... # get a result from the task once it is complete result = future.result() # blocks |
You can learn more about getting results from Future objects in the tutorials:
- How To Get Task Results From the ThreadPoolExecutor in Python
- How to Get Results From The ProcessPoolExecutor in Python
Future Object Timeouts
It is a good practice to limit how long we are willing to wait for a result or an exception.
As such, we can set a timeout when calling these functions via the “timeout” argument and specify the number of seconds.
If the timeout elapses before a result or exception is returned, then a TimeoutError is raised that we may choose to handle.
1 2 3 4 5 6 7 8 |
... # handle any timeout try: # get a result from the task once it is complete result = future.result(timeout=60) # blocks # do something... except TimeoutError: # handle timeout |
Future Object Exception Handling
If an exception is raised during the execution of the task, it will be raised again automatically when we attempt to retrieve the result from the Future.
As such, if an exception can reasonably be raised within the task, then we can handle it when retrieving the result.
1 2 3 4 5 6 7 8 |
... # handle exception raised by the task try: # get a result from the task once it is complete result = future.result() # blocks # do something... except: # handle exception raised when executing the task |
You can learn more about exception handling in Future objects in the tutorials:
- How to Handle Exceptions in Tasks With the ThreadPoolExecutor in Python
ProcessPoolExecutor Exception Handling in Python
Future Object Callbacks
The Future allows us to register a callback function to be called once the task has been completed.
This can be achieved by calling the add_done_callback() function in the Future and specifying the name of our custom callback function.
1 2 3 |
... # register a callback function future.add_done_callback(custom_callback) |
Our custom callback function must take a single argument, which is the Future object on which it is registered.
1 2 3 |
# custom callback function def custom_callback(future): # do something |
The callback is only called once the task has been completed. We can register multiple callback functions for a given Future object and they will be called in the order that they were registered.
An exception in one callback function will not impact the calls to subsequent callback functions.
You can learn more about adding callbacks to Future objects in the tutorials:
- How to Add a Callback to the ThreadPoolExecutor in Python
- How to Add a Callback to the ProcessPoolExecutor in Python
Groups of Future Objects
The concurrent.futures module provides utility functions for working with groups of Future objects.
This includes the concurrent.futures.wait() function that takes a collection of Future objects and blocks and only returns a set of Future objects that match specified conditions, such as:
- The Future object for the first task to complete.
- The Future object for all tasks when they are all completed.
- The Future object for the first task to fail with an exception.
For example:
1 2 3 |
... # wait for all tasks _ = concurrent.futures.wait(futures) |
You can learn more about how to use the concurrent.futures.wait() function in the tutorials:
- How to Wait For The First Task To Finish In The ThreadPoolExecutor
- How to Wait For All Tasks to Finish in the ThreadPoolExecutor
The module also provides the concurrent.futures.as_completed() function that takes a collection of Future objects and yields one object at a time in the order that tasks are completed.
For example:
1 2 3 4 |
... # iterate future objects as they are completed for future in as_completed(futures): # do something... |
You can learn more about the concurrent.futures.as_completed() function in the tutorials:
- How to Use as_completed() with the ThreadPoolExecutor in Python
- Get Results As Tasks Are Completed With ProcessPoolExecutor in Python
Next, let’s take a closer look at the asyncio.Future class.
Run loops using all CPUs, download your FREE book to learn how.
What is asyncio.Future
An asyncio.Future class is a lower-level class in the asyncio module that represents a result that will eventually arrive.
Future objects are used to bridge low-level callback-based code with high-level async/await code.
— asyncio — Asynchronous I/O » Futures
Generally, we do not create Future objects in an asyncio program. Instead, we issue coroutines for asynchronous execution and receive asyncio.Task objects in return.
These asyncio.Task objects extend the asyncio.Future class. As such the asyncio.Future provides a base class for asynchronous tasks executed within the asyncio event loop.
For example, we can issue a coroutine as a task for asynchronous execution as an asyncio.Task using the asyncio.create_task() function.
1 2 3 4 5 |
... # create a coroutine coro = task_coroutine() # create a task from a coroutine task = asyncio.create_task(coro) |
Because the asyncio.Task class extends the asyncio.Future class, it inherits all of its methods.
As such, both classes share the majority of their functionality and talk about asyncio.Task objects are nearly equivalent to talking about asyncio.Future objects. We will talk about both asyncio tasks and futures interchangeably in this section.
You can learn more about asyncio.Task in the tutorial:
You can learn more about how to create an asyncio.Task in the tutorial:
Like a concurrent.futures.Future, the asyncio.Future (or Task) provides a handle on a task that is executing asynchronously in the program.
As such, we can interact with the task via the asyncio.Future representation of the task, such as:
- Checking the status of the task, such as whether it is done or canceled.
- Getting results from the task, e.g. return values, once the task is complete.
- Providing access to exceptions raised in the task while running.
- Adding callback functions to execute automatically once the task is done.
Let’s take a closer look at some of these use cases.
Future Object Status
After an asyncio.Future is created, we can check the status of the task.
There are two statuses we might want to check, they are:
- Whether the task is done via the done() method.
- Whether the task was canceled via the cancelled() method.
We can check if a task is done via the done() method.
The method returns True if the task is done, or False otherwise.
For example:
1 2 3 4 |
... # check if a task is done if task.done(): # ... |
A task is done if it has had the opportunity to run and is now no longer running.
A task that has been scheduled is not done.
Similarly, a task that is running is not done.
A task is done if:
- The coroutine finishes normally.
- The coroutine returns explicitly.
- An unexpected error or exception is raised in the coroutine
- The task is canceled.
You can learn more about checking the status of a task in the tutorial:
Future Object Results
We can get the result of a Future via the result() method.
This 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:
1 2 3 |
... # get the return value from the wrapped coroutine value = task.result() |
You can learn more about getting asyncio.Task results in the tutorial:
Future Object Exception Handling
A coroutine wrapped by a task may raise an exception that is not handled.
This will cancel the task, in effect.
We can retrieve an unhandled exception in the coroutine wrapped by a task via the exception() method.
For example:
1 2 3 |
... # get the exception raised by a task exception = task.exception() |
If the coroutine raises an unhandled error or exception, it is re-raised when calling the result() method and may need to be handled.
For example:
1 2 3 4 5 6 |
... try: # get the return value from the wrapped coroutine value = task.result() except Exception: # task failed and there is no result |
You can learn more about handling Task exceptions in the tutorial:
Cancel Future Object
We can cancel a scheduled task via the cancel() method.
The cancel method returns True if the task was canceled, or False otherwise.
For example:
1 2 3 |
... # cancel the task was_cancelled = task.cancel() |
If the task is already done, it cannot be canceled and the cancel() method will return False and the task will not have the status of canceled.
The next time the task is given an opportunity to run, it will raise a CancelledError exception.
If the CancelledError exception is not handled within the wrapped coroutine, the task will be canceled.
Otherwise, if the CancelledError exception is handled within the wrapped coroutine, the task will not be canceled.
The cancel() method can also take a message argument which will be used in the content of the CancelledError.
You can learn more about canceling a task in the tutorial:
Future Object Callbacks
We can add a done callback function to a task via the add_done_callback() method.
This method takes the name of a function to call when the task is done.
The callback function must take the Task instance as an argument.
1 2 3 4 5 6 7 |
# done callback function def handle(task): print(task) ... # register a done callback function task.add_done_callback(handle) |
Recall that a task may be done when the wrapped coroutine finishes normally, when it returns, when an unhandled exception is raised, or when the task is canceled.
The add_done_callback() method can be used to add or register as many done callback functions as we like.
You can learn more about task done callback functions in the tutorial:
Groups of Future Objects
The asyncio module provides utility functions for working with groups of asyncio.Future (or asyncio.Task) objects.
This includes the asyncio.gather(), asyncio.wait(), and asyncio.as_completed() functions.
The asyncio.gather() module function allows the caller to group multiple awaitables together. These can be Futures, Tasks, or coroutines.
For example:
1 2 3 |
... # run a collection of awaitables results = await asyncio.gather(coro1(), asyncio.create_task(coro2())) |
You can learn about the asyncio.gather() function in the tutorial:
The asyncio.wait() function can be used to wait for a collection of asyncio Future or Task objects to complete.
Three different conditions can be waited for, including:
- Wait for all tasks to complete.
- Wait for the first task to complete.
- Wait for the first task to complete with an exception.
For example:
1 2 3 |
... # wait for all tasks to complete done, pending = await asyncio.wait(tasks) |
You can learn more about the asyncio.wait() function in the tutorial:
The asyncio.as_completed() function will run a collection of tasks and coroutines concurrently.
More importantly, it returns an iterable that we can use to retrieve the awaitables in the order that they are completed.
For example:
1 2 3 4 5 |
... # iterate over awaitables for task in asyncio.as_completed(tasks): # get the next result result = await task |
You can learn more about the asyncio.as_completed() function in the tutorial:
Now that we are familiar with both types of Future classes, let’s consider their similarities and differences.
Similarities Between concurrent.futures.Future and asyncio.Future
The concurrent.futures.Future and asyncio.Future classes have many similarities.
This is not surprising as the asyncio.Future class was developed using the concurrent.futures.Future as inspiration.
The asyncio.Future class was designed based on the concurrent.futures.Future.
The Future object was designed to mimic concurrent.futures.Future.
— asyncio — Asynchronous I/O » Futures
The main similarities between the concurrent.futures.Future and asyncio.Future classes are as follows:
- Both the concurrent.futures.Future and asyncio.Future classes represent a task that is executed asynchronously. The concurrent.futures.Future represents a task executing in a new Thread or Process, whereas the asyncio.Future represents a task executing in the asyncio event loop.
- Both the concurrent.futures.Future and asyncio.Future objects are not created directly, instead, instances are returned from the framework when an asynchronous task is issued.
- Both the concurrent.futures.Future and asyncio.Future classes provide insight into the status of the executing task via methods like done() and cancelled().
- Both the concurrent.futures.Future and asyncio.Future classes provide access to the return value from the task function running asynchronously via the result() method which blocks until the task is complete.
- Both the concurrent.futures.Future and asyncio.Future classes support the addition of done callback functions to be executed automatically when the asynchronous task is completed.
Now that we are familiar with the main similarities between the concurrent.futures.Future and asyncio.Future classes, let’s consider some main differences.
Free Python Asyncio Course
Download your FREE Asyncio PDF cheat sheet and get BONUS access to my free 7-day crash course on the Asyncio API.
Discover how to use the Python asyncio module including how to define, create, and run new coroutines and how to use non-blocking I/O.
Differences Between concurrent.futures.Future and asyncio.Future
Although both the concurrent.futures.Future and asyncio.Future classes are very similar, there are some important differences.
The main differences between the concurrent.futures.Future and asyncio.Future classes are as follows:
- Both the concurrent.futures.Future and asyncio.Future classes are in different modules. This is self-evident but is important to highlight. Because they are in different modules, they are different classes, and any type check that assumes one class and gets the other will fail, e.g. asyncio.isfuture().
- Instances of the asyncio.Future can be awaited in the asyncio event loop, whereas instances of the concurrent.futures.Future cannot be awaited in asyncio. Attempting to do so will result in an error.
- Instances of the concurrent.futures.Future class cannot be used in asyncio module functions like wait() and as_completed(), similarly, instances of asyncio.Future cannot be used in concurrent.futures module functions like wait() and as_completed().
- The asyncio.Future methods that block do not support timeouts, whereas the concurrent.futures.Future methods that block like result() and exception() support a “timeout” argument.
- Instances of the asyncio.Future class can be canceled while running (but blocked) whereas instances of the concurrent.futures.Future class can only be canceled if they are scheduled but not yet running.
Further, because the asyncio.Future class is inspired by the concurrent.futures.Future class, the API documentation for the asyncio.Future includes notes on how the two classes differ:
* unlike asyncio Futures, concurrent.futures.Future instances cannot be awaited.
— asyncio — Asynchronous I/O » Futures
* asyncio.Future.result() and asyncio.Future.exception() do not accept the timeout argument.
* asyncio.Future.result() and asyncio.Future.exception() raise an InvalidStateError exception when the Future is not done.
* Callbacks registered with asyncio.Future.add_done_callback() are not called immediately. They are scheduled with loop.call_soon() instead.
* asyncio Future is not compatible with the concurrent.futures.wait() and concurrent.futures.as_completed() functions.
* asyncio.Future.cancel() accepts an optional msg argument, but concurrent.futures.cancel() does not.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Further Reading
This section provides additional resources that you may find helpful.
Python Asyncio Books
- Python Asyncio Mastery, Jason Brownlee (my book!)
- Python Asyncio Jump-Start, Jason Brownlee.
- Python Asyncio Interview Questions, Jason Brownlee.
- Asyncio Module API Cheat Sheet
I also recommend the following books:
- Python Concurrency with asyncio, Matthew Fowler, 2022.
- Using Asyncio in Python, Caleb Hattingh, 2020.
- asyncio Recipes, Mohamed Mustapha Tahrioui, 2019.
Guides
APIs
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
References
Takeaways
You now know how the concurrent.futures.Future and asyncio.Future classes are both similar and different.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Benjamin Golinvaux says
Hello Jason,
Thanks for this comprehensive and excellent article. I particularly appreciate the links in each section that allow to know more about the various topics that you are mentioning.
I am a bit puzzled by what appears, to my beginner eyes, as a contradiction.
You wrote, in the article above:
“Both the concurrent.futures.Future and asyncio.Future classes provide access to the return value from the task function running asynchronously via the result() method which blocks until the task is complete.”
Yet, in both the asyncio Future doc and in your “Asyncio Event Loop in Separate Thread” article, you mention that .result(), in the asyncio Future, will raise an InvalidStateException if the result is not available yet (contrary to concurrent.futures.Future that will block until the result is available).
It seems that I am missing something.
It’s a small detail, but if you could comment on this , it would be great.
Thanks a lot in advance
Jason Brownlee says
Thank you kindly, I believe you are correct. I must have been working from (incorrect) memory when writing that part.