How to use asyncio.TaskGroup - Super Fast Python

A problem with tasks is that it is a good idea to assign and keep track of the asyncio.Task objects.

The reason is that if we don’t the tasks may be garbage collected, terminating the task.

A helpful solution is to use a TaskGroup to create and manage a collection of tasks. It has a number of benefits, such as canceling all tasks in the group if one task fails.

In this tutorial, you will discover how to use the TaskGroup in asyncio.

After completing this tutorial, you will know:

  • How to use the TaskGroup to create and manage a collection of tasks.
  • How to wait for a collection of tasks to complete using the TaskGroup context manager interface.
  • How to cancel all tasks if one task in the TaskGroup fails.

Let’s get started.

Need To Manage Multiple Coroutines as a Group

It is common to issue many coroutines and then manage them as a group.

Treating multiple coroutines as a group allows for functionality such as:

  1. Waiting until all tasks are completed.
  2. Canceling all tasks if one task fails.
  3. Handling an exception raised in any task.

Prior to Python 3.11, there were two main approaches to handling multiple coroutines as a group, they were:

  1. Call asyncio.gather()
  2. Call asyncio.wait()

Manage Multiple Coroutines with asyncio.gather()

The asyncio.gather() function takes one more coroutine or asyncio.Task objects.

It returns a Future object that allows the group of tasks to be managed together with features such as canceling all tasks and waiting on all tasks.

You can learn more about how to use the asyncio.gather() function in the tutorial:

It is possible to use cancel all tasks in the group if one task in the group fails with an exception.

You can see an example of this in the tutorial:

Manage Multiple Coroutines with asyncio.wait()

The asyncio.wait() function takes a collection of coroutines or tasks and returns the set of tasks that meet the specified conditions, such as one completed, all completed or first to fail.

You can learn more about how to use the asyncio.wait() function in the tutorial:

The release of Python version 3.11 introduced a new approach to managing multiple coroutines or tasks as a group, called the asyncio.TaskGroup.

Python 3.11 introduce the asyncio.TaskGroup task for managing a group of associated asyncio task.

Added the TaskGroup class, an asynchronous context manager holding a group of tasks that will wait for all of them upon exit. For new code this is recommended over using create_task() and gather() directly.

What’s New In Python 3.11

The asyncio.TaskGroup class is intended as a replacement for the asyncio.create_task() function for creating tasks and the asyncio.gather() function for waiting on a group of tasks.

Historically, we create and issue a coroutine as an asyncio.Task using the asyncio.create_task() function.

For example:

...

# create and issue coroutine as task

task = asyncio.create_task(coro())

This creates a new asyncio.Task object and issues it to the asyncio event loop for execution as soon as it is able.

We can then choose to await the task and wait for it to be completed.

For example:

...

# wait for task to complete

result = await task

You can learn more about executing coroutines as asyncio.Task objects in the tutorial:

As we have seen, the asyncio.gather() function is used to create and issue many coroutines simultaneously as asyncio.Task objects to the event loop, allowing the caller to treat them all as a group.

The most common usage is to wait for all issued tasks to complete.

For example:

...

# issue coroutines as tasks and wait for them to complete

results = await asyncio.gather(coro1(), coro2(), coro2)

The asyncio.TaskGroup can perform both of these activities and is the preferred approach.

An asynchronous context manager holding a group of tasks. Tasks can be added to the group using create_task(). All tasks are awaited when the context manager exits.

Asyncio Task Groups

How to Create an asyncio.TaskGroup

An asyncio.TaskGroup object implements the asynchronous context manager interface, and this is the preferred usage of the class.

This means that an instance of the class is created and is used via the “async with” expression.

For example:

...

# create a taskgroup

async with asyncio.TaskGroup() as group:

# ...

If you are new to the “async with” expression, see the tutorial:

Recall that an asynchronous context manager implements the __aenter__() and __aexit__() methods which can be awaited.

In the case of the asyncio.TaskGroup, the __aexit__() method which is called automatically when the context manager block is exited will await all tasks created by the asyncio.TaskGroup.

This means that exiting the TaskGroup object’s block normally or via an exception will automatically await until all group tasks are done.

...

# create a taskgroup

async with asyncio.TaskGroup() as group:

# ...

# wait for all group tasks are done

You can learn more about asynchronous context managers in the tutorial:

How to Create Tasks Using asyncio.TaskGroup

We can create a task in the task group via the create_task() method on the asyncio.TaskGroup object.

For example:

...

# create a taskgroup

async with asyncio.TaskGroup() as group:

# create and issue a task

task = group.create_task(coro())

This will create an asyncio.Task object and issue it to the asyncio event loop for execution, just like the asyncio.create_task() function, except that the task is associated with the group.

We can await the task directly if we choose and get results.

For example:

...

# create a taskgroup

async with asyncio.TaskGroup() as group:

# create and issue a task

result = await group.create_task(coro())

The benefit of using the asyncio.TaskGroup is that we can issue multiple tasks in the group and execute code in between. such as checking results or gathering more data.

How to Wait on Tasks Using asyncio.TaskGroup

We can wait on all tasks in the group by exiting the asynchronous context manager block.

As such, the tasks are awaited automatically and nothing additional is required.

For example:

...

# create a taskgroup

async with asyncio.TaskGroup() as group:

# ...

# wait for all group tasks are done

If this behavior is not preferred, then we must ensure all tasks are “done” (finished, canceled, or failed) before exiting the context manager.

How to Cancel All Tasks If One Task Fails Using asyncio.TaskGroup

If one task in the group fails with an exception, then all non-done tasks remaining in the group will be canceled.

This is performed automatically and does not require any additional code.

For example:

# handle the failure of any tasks in the group

try:

...

# create a taskgroup

async with asyncio.TaskGroup() as group:

# create and issue a task

task1 = group.create_task(coro1())

# create and issue a task

task2 = group.create_task(coro2())

# create and issue a task

task3 = group.create_task(coro3())

# wait for all group tasks are done

except:

# all non-done tasks are cancelled

pass

If this behavior is not preferred, then the failure of each task must be managed within the tasks themselves, e.g. by a try-except block within the coroutine.

Now that we know how to use the asyncio.TaskGroup, let’s look at some worked examples.

Example of Waiting on Multiple Tasks with a TaskGroup

We can explore the case of creating multiple tasks within an asyncio.TaskGroup and then waiting for all tasks to complete.

This can be achieved by first defining a suite of different coroutines that represent the tasks we want to complete.

In this case, we will define 3 coroutines that each report a different message and then sleep for one second.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

# coroutine task

async def task1():

    # report a message

    print('Hello from coroutine 1')

    # sleep to simulate waiting

    await asyncio.sleep(1)

# coroutine task

async def task2():

    # report a message

    print('Hello from coroutine 2')

    # sleep to simulate waiting

    await asyncio.sleep(1)

# coroutine task

async def task3():

    # report a message

    print('Hello from coroutine 3')

    # sleep to simulate waiting

    await asyncio.sleep(1)

Next, we can define a main() coroutine that creates the asyncio.TaskGroup via the context manager interface.

# asyncio entry point

async def main():

    # create task group

    async with asyncio.TaskGroup() as group:

    # ...

We can then create and issue each coroutine as a task into the event loop, although collected together as part of the group.

...

# run first task

group.create_task(task1())

# run second task

group.create_task(task2())

# run third task

group.create_task(task3())

Notice that we don’t need to keep a reference to the asyncio.Task objects as the asyncio.TaskGroup will keep track of them for us.

Also, notice that we don’t need to await the tasks because when we exit the context manager block for the asyncio.TaskGroup we will await all tasks in the group.

Tying this together, the complete example is listed below.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

# example of asyncio task group

import asyncio

# coroutine task

async def task1():

    # report a message

    print('Hello from coroutine 1')

    # sleep to simulate waiting

    await asyncio.sleep(1)

# coroutine task

async def task2():

    # report a message

    print('Hello from coroutine 2')

    # sleep to simulate waiting

    await asyncio.sleep(1)

# coroutine task

async def task3():

    # report a message

    print('Hello from coroutine 3')

    # sleep to simulate waiting

    await asyncio.sleep(1)

# asyncio entry point

async def main():

    # create task group

    async with asyncio.TaskGroup() as group:

        # run first task

        group.create_task(task1())

        # run second task

        group.create_task(task2())

        # run third task

        group.create_task(task3())

    # wait for all tasks to complete...

    print('Done')

# entry point

asyncio.run(main())

Running the example first executes the main() coroutine, starting a new event loop for us.

The main() coroutine runs and creates an asyncio.TaskGroup.

All three coroutines are then created as asyncio.Task objects and issued to the event loop via the asyncio.TaskGroup.

The context manager block for the asyncio.TaskGroup is exited which automatically awaits all three tasks.

The tasks report their message and sleep.

Once all tasks are completed the main() coroutine reports a final message.

Hello from coroutine 1

Hello from coroutine 2

Hello from coroutine 3

Done

Next, let’s explore how we might use an asyncio.TaskGroup with tasks that take arguments and return values.


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.

Learn more
 


Example of TaskGroup with Arguments and Return Values

We can explore the case of executing coroutines as tasks that take arguments and return values.

These are just like coroutines we might issue normally as tasks without the asyncio.TaskGroup, but it is good to have an example for reference.

In this case, we will define a task that takes an argument, sleeps, then returns the argument multiplied by 100.

# coroutine task

async def task(value):

    # sleep to simulate waiting

    await asyncio.sleep(1)

    # return value

    return value * 100

The main coroutine will then create an asyncio.TaskGroup and then create 9 instances of the task, passing the value 1 to 9 as arguments to the task.

The task objects are kept so we can retrieve the values from them later. This is achieved using a list comprehension.

Once all tasks are complete, the return values are retrieved and reported.

# asyncio entry point

async def main():

    # create task group

    async with asyncio.TaskGroup() as group:

        # create and issue tasks

        tasks = [group.create_task(task(i)) for i in range(1,10)]

    # wait for all tasks to complete...

    # report all results

    for t in tasks:

        print(t.result())

Tying this together, the complete example is listed below.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

# example of asyncio task group with return values

import asyncio

# coroutine task

async def task(value):

    # sleep to simulate waiting

    await asyncio.sleep(1)

    # return value

    return value * 100

# asyncio entry point

async def main():

    # create task group

    async with asyncio.TaskGroup() as group:

        # create and issue tasks

        tasks = [group.create_task(task(i)) for i in range(1,10)]

    # wait for all tasks to complete...

    # report all results

    for t in tasks:

        print(t.result())

# entry point

asyncio.run(main())

Running the example first executes the main() coroutine, starting a new event loop for us.

The main() coroutine runs and creates an asyncio.TaskGroup.

A total of 9 coroutines are issued as tasks via the asyncio.TaskGroup and the asyncio.Task objects are stored in a list.

The main() coroutine then awaits all tasks.

Each task runs, sleeps, then returns its input argument multiples by one hundred.

Once all tasks are complete, the asyncio.Task objects are iterated and the return value is reported from each.

This shows how we might pass arguments to tasks created via the asyncio.TaskGroup and how we might keep track of asyncio.Task objects in order to manually retrieve results from each task at a later stage.

100

200

300

400

500

600

700

800

900

Next, let’s look at an example of canceling all tasks in the group if one task fails.

Example of Cancelling All Tasks if One Task Fails Using TaskGroup

We can explore the case of canceling all tasks in the asyncio.TaskGroup if one task fails.

A failed task means that a coroutine is executed in an asyncio.Task object that raises an exception that is not handled in the coroutine, meaning that it bubbles up to the task and causes the task to be halted early.

It is common to issue many tasks and cancel all tasks if one or more of the tasks fails.

The asyncio.TaskGroup will perform this action automatically for us.

In this case, we will define 3 different coroutines that report a message and sleep. The second coroutine will then fail with an uncaught exception.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

# coroutine task

async def task1():

    # report a message

    print('Hello from coroutine 1')

    # sleep to simulate waiting

    await asyncio.sleep(1)

# coroutine task

async def task2():

    # report a message

    print('Hello from coroutine 2')

    # sleep to simulate waiting

    await asyncio.sleep(0.5)

    # fail with an exception

    raise Exception('Something bad happened')

# coroutine task

async def task3():

    # report a message

    print('Hello from coroutine 2')

    # sleep to simulate waiting

    await asyncio.sleep(1)

Note that the second task sleeps less than the other two tasks before raising an exception.

This is to ensure that the other two tasks are still running at the point that the second task fails so that we can see if they are canceled as we expect.

The main() coroutine will issue all tasks via the asyncio.TaskGroup and then report the done and cancel status of each in turn once all tasks are “done”.

Recall a “done” task is a task that is finished normally, canceled, or failed with an exception.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

# asyncio entry point

async def main():

    # handle exceptions

    try:

        # create task group

        async with asyncio.TaskGroup() as group:

            # run first task

            t1 = group.create_task(task1())

            # run second task

            t2 = group.create_task(task2())

            # run third task

            t3 = group.create_task(task3())

    except:

        pass

    # check the status of each task

    print(f'Task1: done={t1.done()}, cancelled={t1.cancelled()}')

    print(f'Task2: done={t2.done()}, cancelled={t2.cancelled()}')

    print(f'Task3: done={t3.done()}, cancelled={t3.cancelled()}')

Notice that we wrap the entire asyncio.TaskGroup in an exception as any uncaught exception that occurs in a task is re-raised by the asyncio.TaskGroup

Tying this together, the complete example is listed below.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

# example of asyncio task group with a failed task

import asyncio

# coroutine task

async def task1():

    # report a message

    print('Hello from coroutine 1')

    # sleep to simulate waiting

    await asyncio.sleep(1)

# coroutine task

async def task2():

    # report a message

    print('Hello from coroutine 2')

    # sleep to simulate waiting

    await asyncio.sleep(0.5)

    # fail with an exception

    raise Exception('Something bad happened')

# coroutine task

async def task3():

    # report a message

    print('Hello from coroutine 2')

    # sleep to simulate waiting

    await asyncio.sleep(1)

# asyncio entry point

async def main():

    # handle exceptions

    try:

        # create task group

        async with asyncio.TaskGroup() as group:

            # run first task

            t1 = group.create_task(task1())

            # run second task

            t2 = group.create_task(task2())

            # run third task

            t3 = group.create_task(task3())

    except:

        pass

    # check the status of each task

    print(f'Task1: done={t1.done()}, cancelled={t1.cancelled()}')

    print(f'Task2: done={t2.done()}, cancelled={t2.cancelled()}')

    print(f'Task3: done={t3.done()}, cancelled={t3.cancelled()}')

# entry point

asyncio.run(main())

Running the example first executes the main() coroutine, starting a new event loop for us.

The main() coroutine runs and creates an asyncio.TaskGroup.

The three coroutines are then issued as tasks via the asyncio.TaskGroup and the asyncio.Task objects are kept in local variables for later.

The asyncio.TaskGroup context manager block is exited and the main() coroutine then awaits all three tasks.

The tasks run, report a message and sleep. The second coroutine then fails with an exception.

The asyncio.TaskGroup handles the exception and cancels all remaining not-done tasks. The exception is then re-raised at the top level and ignored.

The done and canceled status of each task is then reported. We can see that all tasks are done and that the two tasks (1 and 3) that were running at the time task 2 failed with an exception were indeed canceled.

This highlights how all running tasks in the group will be canceled if a task in the group fails with an unhanded exception.

It is possible to shield a task from cancellation. You can learn more about this in the tutorial:

Hello from coroutine 1

Hello from coroutine 2

Hello from coroutine 2

Task1: done=True, cancelled=True

Task2: done=True, cancelled=False

Task3: done=True, cancelled=True

Next, let’s look at an example of manually canceling one task in the group.


Python Asyncio Jump-Start

Loving The Tutorials?

Why not take the next step? Get the book.

Learn more
 


Example of Cancelling One Task in a TaskGroup

We can explore the case of manually canceling one task in the group.

This can be achieved by calling the cancel() method on the asyncio.Task object.

If the task is not done, the request to cancel the task will be handled by the task.

You can learn more about canceling tasks in the tutorial:

In this case, we will issue 3 tasks using the asyncio.TaskGroup, wait a moment, then cancel the second task.

The expectation is that only the second task will be canceled and all other tasks will be left to run as per normal. We will confirm this by reporting the “done” and “cancelled” status of all tasks after all tasks are done.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

# asyncio entry point

async def main():

    # create task group

    async with asyncio.TaskGroup() as group:

        # run first task

        t1 = group.create_task(task1())

        # run second task

        t2 = group.create_task(task2())

        # run third task

        t3 = group.create_task(task3())

        # wait a moment

        await asyncio.sleep(0.5)

        # cancel the second task

        t2.cancel()

    # check the status of each task

    print(f'Task1: done={t1.done()}, cancelled={t1.cancelled()}')

    print(f'Task2: done={t2.done()}, cancelled={t2.cancelled()}')

    print(f'Task3: done={t3.done()}, cancelled={t3.cancelled()}')

Tying this together, the complete example is listed below.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

# example of asyncio task group with a canceled task

import asyncio

# coroutine task

async def task1():

    # sleep to simulate waiting

    await asyncio.sleep(1)

    # report a message

    print('Hello from coroutine 1')

# coroutine task

async def task2():

    # sleep to simulate waiting

    await asyncio.sleep(1)

    # report a message

    print('Hello from coroutine 2')

# coroutine task

async def task3():

    # sleep to simulate waiting

    await asyncio.sleep(1)

    # report a message

    print('Hello from coroutine 2')

# asyncio entry point

async def main():

    # create task group

    async with asyncio.TaskGroup() as group:

        # run first task

        t1 = group.create_task(task1())

        # run second task

        t2 = group.create_task(task2())

        # run third task

        t3 = group.create_task(task3())

        # wait a moment

        await asyncio.sleep(0.5)

        # cancel the second task

        t2.cancel()

    # check the status of each task

    print(f'Task1: done={t1.done()}, cancelled={t1.cancelled()}')

    print(f'Task2: done={t2.done()}, cancelled={t2.cancelled()}')

    print(f'Task3: done={t3.done()}, cancelled={t3.cancelled()}')

# entry point

asyncio.run(main())

Running the example first executes the main() coroutine, starting a new event loop for us.

The main() coroutine runs and creates an asyncio.TaskGroup.

The three coroutines are then issued as tasks via the asyncio.TaskGroup and the asyncio.Task objects are kept in local variables for later.

The main() coroutine sleeps for a moment, allowing the tasks to run.

The main() coroutine results and then cancels the second task. It then exits the context manager of the asyncio.TaskGroup and awaits all tasks.

The second task is canceled. The remaining tasks complete normally. We see messages from tasks 1 and 3 only because task 2 was canceled before the message could be reported.

Checking the status of the tasks, we can see that all tasks are done and only task 2 was canceled.

This highlights that we can manually cancel tasks in the group, leaving other tasks unaffected.

Hello from coroutine 1

Hello from coroutine 2

Task1: done=True, cancelled=False

Task2: done=True, cancelled=True

Task3: done=True, cancelled=False

Further Reading

This section provides additional resources that you may find helpful.

Python Asyncio Books

I also recommend the following books:

Guides

APIs

References

Takeaways

You now know how to use the asyncio.TaskGroup in Python.

Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.

Photo by Jonathan Cooper on Unsplash