An asyncio task has a 4-part life-cycle that transitions from created, scheduled, running, and done.
In this tutorial, you will discover the life-cycle of an asyncio Task in Python.
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:
1 2 3 |
... # 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 the life-cycle of a task.
Run loops using all CPUs, download your FREE book to learn how.
Task Life-Cycle
An asyncio Task has a life cycle.
Firstly, a task is created from a coroutine.
It is then scheduled for independent execution within the event loop.
At some point, it will run.
While running it may be suspended, such as awaiting another coroutine or task. It may finish normally and return a result or fail with an exception.
Another coroutine may intervene and cancel the task.
Eventually, it will be done and cannot execute again.
We can summarize this life-cycle as follows:
- 1. Created
- 2. Scheduled
- 2a Canceled
- 3. Running
- 3a. Suspended
- 3b. Result
- 3c. Exception
- 3d. Canceled
- 4. Done
Note that Suspended, Result, Exception, and Canceled are not states per se, they are important points transition for a running task.
The diagram below summarizes this life cycle showing the transitions between each phase.
Now that we are familiar with the life cycle of a task from a high level, let’s take a closer look at each phase.
Step 1: Created
Generally, we do not create an asyncio.Task object from a coroutine object directly.
Instead, we use a factory function that takes a coroutine and returns an asyncio.Task object.
A task can only be created from within a coroutine.
There are 3 ways you can create an asyncio Task from a coroutine, they are:
- Create a Task with asyncio.create_task() (recommended)
- Create a Task with asyncio.ensure_future() (low-level)
- Create a Task with loop.create_task() (low-level)
Let’s take a closer look at each in turn:
Create a Task with asyncio.create_task()
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:
1 2 3 |
... # create and schedule a task task = asyncio.create_task(coro) |
We can specify a name for the task. This allows the same coroutine to be scheduled many times although each potentially with a different name, allowing them to be differentiated programmatically.
This can be achieved via the “name” argument.
For example:
1 2 3 |
... # create and schedule a task with a name task = asyncio.create_task(coro, name='MyTask') |
The asyncio.create_task() is a high-level asyncio API and is the preferred way to create Tasks in our asyncio programs.
Create a Task with asyncio.ensure_future()
We can create a task using the asyncio.ensure_future() function.
This function takes a Future, Task, Future-like object, or a coroutine as an argument.
It will then schedule the task for execution and return a Task instance. If a coroutine is provided to the function, then it is wrapped for us in a Task instance, which is returned.
Return: a Task object wrapping obj, if obj is a coroutine (iscoroutine() is used for the test); in this case the coroutine will be scheduled by ensure_future().
— Futures
For example:
1 2 3 |
... # create and schedule a task task = asyncio.ensure_future(coro) |
The asyncio.ensure_future() allows the caller to specify the event loop used to schedule the task via the “loop” argument. By default, it will use the current event loop that is executing the coroutine that is creating the task.
This is a low-level asyncio API and is generally not recommended for creating tasks in asyncio programs.
Future objects are used to bridge low-level callback-based code with high-level async/await code.
— Futures
Create a Task with loop.create_task()
We can create tasks via the create_task() method on an event loop instance.
This requires that we first get an event loop instance, such as the currently running event loop. This can be achieved via the asyncio.get_event_loop() function.
For example:
1 2 3 |
... # get the current event loop loop = asyncio.get_event_loop() |
Once we have an event loop instance, we can call the create_task() method on the event loop and pass it a coroutine. It will then return a Task instance that wraps the provided coroutine and schedules it for execution.
Schedule the execution of coroutine coro. Return a Task object.
— Event Loop
For example:
1 2 3 |
... # create and schedule a task task = loop.create_task(coro) |
Like the asyncio.create_task() function, the loop.create_task() method takes a “name” argument that can be used to assign a logical name to the task.
For example:
1 2 3 |
... # create and schedule a task task = loop.create_task(coro, name='MyTask') |
Like the asyncio.ensure_future() function, the loop.create_task() method is a low-level asyncio API and is not the preferred way to create and schedule tasks in asyncio programs.
Application developers should typically use the high-level asyncio functions, such as asyncio.run(), and should rarely need to reference the loop object or call its methods. This section is intended mostly for authors of lower-level code, libraries, and frameworks, who need finer control over the event loop behavior.
— Event Loop
You can learn more about creating a Task in the tutorial:
Next, let’s take a look at the scheduled phase of a task.
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.
Step 2: Scheduled
After a task is created, it is scheduled.
We do not schedule the task ourselves.
Instead, the task is scheduled in the asyncio event loop automatically.
While scheduled a task is not done and is not running.
We do not have a lot of insight into a task in the scheduled state.
For example, there is no “scheduled” method on the asyncio.Task object to check if the task is in the scheduled but not yet in the running state.
If we print a Task object directly after it has been scheduled, it will have the status “pending” and “running“.
For example:
1 2 |
... <Task pending name='Task-2' coro=<task_coroutine() running at ...>> |
A scheduled task can be canceled, but the request to cancel is not handled until the task begins running.
Therefore, from a practical perspective, a scheduled task can be treated like a running task.
Next, let’s take a look at the running phase of a task.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Step 3: Running
The goal of creating a task is to run the task.
We can determine if a Task object is running (or scheduled) from another coroutine by calling the done() method.
This method returns False if the task is running or scheduled or True if the task is done.
For example:
1 2 3 4 |
... # check if a task is running (or scheduled) if not task.done(): # ... |
A running task can be suspended or can transition to the done state.
Specifically, the main points of transition for a running task are as follows:
- Suspended
- Result
- Exception
- Canceled
Step 3a. Suspended
A task can suspend itself.
This can be achieved if it awaits another task or coroutine.
This may be explicit via the await expression.
For example:
1 2 3 |
... # suspend this running task await asyncio.sleep(1) |
It may be less obvious, such as if the task executes an asynchronous generator, iterator or context manager.
For example:
1 2 3 4 |
... # suspend this running task async for item in items: # ... |
When a task is suspended, it will be marked internally as “pending” and “running“.
There are no methods on the Task object to determine this state, but we can see it if we print the Task object.
For example:
1 2 |
... <Task pending name='Task-2' coro=<task_coroutine() running at ...> wait_for=<Future pending cb=[Task.task_wakeup()]>> |
Here, we can see the task is marked “pending” and “running” and is waiting on a call to task_wakeup().
A suspended task can transition to a running task again.
Even if a suspended task is canceled, it must run first in order to respond to the request to cancel.
Step 3b. Result
A task has a result if it finishes normally.
This means it reaches the end of the coroutine or uses the “return” expression explicitly.
It may return a value, otherwise, it will return a None value.
The return value can be retrieved from the task via the result() method.
A result indicates the task is transitioned to the done state.
You can learn more about getting the result from a task in the tutorial:
Step 3c. Exception
A task may raise an exception that is not handled.
This will terminate the task and transition it to the done state.
An unhandled exception will not terminate the task, instead, it will be caught by the asyncio event loop.
If a caller attempts to get a result from the Task object via the result() method, the exception will be re-raised.
If a task is done because it raised an unhandled exception, it will be marked as “finished” and “done, and the details of the exception will be indicated when the Task object is printed.
For example:
1 2 |
... <Task finished name='Task-2' coro=<task_coroutine() done, defined at ...> exception=Exception('error')> |
We can determine if a task transitioned to the done state via an exception by calling the exception() method on the Task, which will return an Exception instance or None if no exception was raised.
For example:
1 2 3 4 |
... # check if a task failed with an exception if task.exception() != None: # ... |
You can learn more about handling exceptions in tasks in the tutorial:
Step 3d. Canceled
A running task can be canceled.
Another task or coroutine may call the cancel() method on the Task object.
The next time the task executes it will raise a CancelledError exception. If the coroutine that the Task wraps does not handle this exception, it will terminate the task and transition it to the done state.
If the coroutine does handle the CancelledError exception, then the task may not be terminated.
As such, canceling a task is a request at best.
When a task is canceled it is marked as “cancelled” and “done“.
We can see this if we print a canceled Task object.
For example:
1 2 |
... <Task cancelled name='Task-2' coro=<task_coroutine() done, defined at ...>> |
We can also determine that a task was canceled programmatically by calling the cancelled() method.
This will return True if the Task was running and was canceled, e.g. the CancelledError exception terminated the running coroutine.
For example:
1 2 3 4 |
... # check if a task was canceled if task.cancelled(): # ... |
You can learn more about canceling tasks in the tutorial:
Next, let’s take a look at the scheduled phase of a task.
Step 4: Done
A task is done if it is no longer running (or scheduled).
A task becomes done in one of three main ways, they are:
- The task finished normally and may have returned a result.
- The task failed with an unhandled exception.
- The task was canceled.
When a task is done it will have the status “done“.
If the task finishes normally, it will also have the status “finished“.
We can see this if we print the Task object.
For example:
1 2 |
... <Task finished name='Task-2' coro=<task_coroutine() done, defined at ...> result=None> |
We can determine if a task is done via the done() method, which will return True if the task is indeed finished or False otherwise.
For example:
1 2 3 4 |
... # check if a task is done if task.done(): # ... |
Only after the task is done, any done callback functions are called.
This means that when the callback functions are executing, the task is “done“, although may finish normally, fail with an exception, or have been canceled.
Recall done callback functions are added via the add_done_callback() method.
For example:
1 2 3 |
... # add a done callback function task.add_done_callback() |
You can learn more about done callback functions in the tutorial:
A done task cannot transition back to scheduled or running. For example, we cannot reuse or restart a task.
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 the life-cycle of an asyncio Task in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Do you have any questions?