Asyncio Shield From Cancellation - Super Fast Python

Asyncio tasks can be canceled at any time.

This can cause a running task to stop mid-execution, which can cause problems if we expect a task or subtask to complete as an atomic operation.

Asyncio provides a way to shield tasks from cancellation via asyncio.shield(), making them directly immune from being canceled.

In this tutorial, you will discover how to shield asyncio tasks from cancellation.

After completing this tutorial, you will know:

  • How to shield asyncio tasks from cancellation.
  • How shields work and how we can work around them if we really need to.
  • How to develop worked examples that use shields to protect tasks from cancellation.

Let’s get started.

What is Asyncio shield()

The asyncio.shield() function wraps an awaitable in Future that will absorb requests to be canceled.

Protect an awaitable object from being cancelled.

Coroutines and Tasks

This means the shielded future can be passed around to tasks that may try to cancel it and the cancellation request will look like it was successful, except that the Task or coroutine that is being shielded will continue to run.

It may be useful in asyncio programs where some tasks can be canceled, but others, perhaps with a higher priority cannot.

It may also be useful in programs where some tasks can safely be canceled, such as those that were designed with asyncio in mind, whereas others cannot be safely terminated and therefore must be shielded from cancellation.

Now that we know what asyncio.shield() is, let’s look at how to use it.

How to Use Asyncio shield()

The asyncio.shield() function will protect another Task or coroutine from being canceled.

It takes an awaitable as an argument and returns an asyncio.Future object.

The Future object can then be awaited directly or passed to another task or coroutine.

For example:

...

# shield a task from cancellation

shielded = asyncio.shield(task)

# await the shielded task

await shielded

The returned Future can be canceled by calling the cancel() method.

If the inner task is running, the request will be reported as successful.

For example:

...

# cancel a shielded task

was_canceld = shielded.cancel()

You can learn more about canceling asyncio tasks in the tutorial:

Any coroutines awaiting the Future object will raise an asyncio.CancelledError, which may need to be handled.

For example:

...

try:

# await the shielded task

await asyncio.shield(task)

except asyncio.CancelledError:

# ...

Importantly, the request for cancellation made on the Future object is not propagated to the inner task.

This means that the request for cancellation is absorbed by the shield.

For example:

...

# create a task

task = asyncio.create_task(coro())

# create a shield

shield = asyncio.shield(task)

# cancel the shield (does not cancel the task)

shield.cancel()

If a coroutine is provided to the asyncio.shield() function it is wrapped in an asyncio.Task() and scheduled immediately.

This means that the shield does not need to be awaited for the inner coroutine to run.

If aw is a coroutine it is automatically scheduled as a Task.

Coroutines and Tasks

If the task that is being shielded is canceled, the cancellation request will be propagated up to the shield, which will also be canceled.

For example:

...

# create a task

task = asyncio.create_task(coro())

# create a shield

shield = asyncio.shield(task)

# cancel the task (also cancels the shield)

task.cancel()

Now that we know how to use the asyncio.shield() function, let’s look at some worked examples.

Example of Asyncio shield() for a Task

We can explore how to protect a task from cancellation using asyncio.shield().

In this example, we define a simple coroutine task that takes an integer argument, sleeps for a second, then returns the argument. The coroutine can then be created and scheduled as a Task.

We can define a second coroutine that takes a task, sleeps for a fraction of a second, then cancels the provided task.

In the main coroutine, we can then shield the first task and pass it to the second task, then await the shielded task.

The expectation is that the shield will be canceled and leave the inner task intact. The cancellation will disrupt the main coroutine. We can check the status of the inner task at the end of the program and we expect it to have been completed normally, regardless of the request to cancel made on the shield.

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

# SuperFastPython.com

# example of using asyncio shield to protect a task from cancellation

import asyncio

# define a simple asynchronous

async def simple_task(number):

    # block for a moment

    await asyncio.sleep(1)

    # return the argument

    return number

# cancel the given task after a moment

async def cancel_task(task):

    # block for a moment

    await asyncio.sleep(0.2)

    # cancel the task

    was_cancelled = task.cancel()

    print(f'cancelled: {was_cancelled}')

# define a simple coroutine

async def main():

    # create the coroutine

    coro = simple_task(1)

    # create a task

    task = asyncio.create_task(coro)

    # created the shielded task

    shielded = asyncio.shield(task)

    # create the task to cancel the previous task

    asyncio.create_task(cancel_task(shielded))

    # handle cancellation

    try:

        # await the shielded task

        result = await shielded

        # report the result

        print(f'>got: {result}')

    except asyncio.CancelledError:

        print('shielded was cancelled')

    # wait a moment

    await asyncio.sleep(1)

    # report the details of the tasks

    print(f'shielded: {shielded}')

    print(f'task: {task}')

# start

asyncio.run(main())

Running the example first creates the main() coroutine and uses it as the entry point into the application.

The task coroutine is created, then it is wrapped and scheduled in a Task.

The task is then shielded from cancellation.

The shielded task is then passed to the cancel_task() coroutine which is wrapped in a task and scheduled.

The main coroutine then awaits the shielded task, which expects a CancelledError exception.

The task runs for a moment then sleeps. The cancellation task runs for a moment, sleeps, resumes then cancels the shielded task. The request to cancel reports that it was successful.

This raises a CancelledError exception in the shielded Future, although not in the inner task.

The main() coroutine resumes and responds to the CancelledError exception, reporting a message. It then sleeps for a while longer.

The task resumes, finishes, and returns a value.

Finally, the main() coroutine resumes, and reports the status of the shielded future and the inner task. We can see that the shielded future is marked as canceled and yet the inner task is marked as finished normally and provides a return value.

This example highlights how a shield can be used to successfully protect an inner task from cancellation.

cancelled: True

shielded was cancelled

shielded: <Future cancelled>

task: <Task finished name='Task-2' coro=<simple_task() done, defined at ...> result=1>

Next, let’s look at shielding a coroutine, instead of a task, from cancellation.


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 Asyncio shield for a Coroutine

We can explore how to protect a coroutine from cancellation using asyncio.shield().

In this example, we update the above example to shield a coroutine directly, instead of a task.

The expectation is that the coroutine will be wrapped in a task immediately and scheduled for execution. The request for cancellation made on the shield will protect the inner coroutine from cancellation as it does for tasks.

At the end of the program, we will locate the asyncio.Task associated with the shielded coroutine and report its status. The expectation is that it will have finished normally and was not canceled.

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

48

# SuperFastPython.com

# example of using asyncio shield to protect a coroutine from cancellation

import asyncio

# define a simple asynchronous

async def simple_task(number):

    # block for a moment

    await asyncio.sleep(1)

    # return the argument

    return number

# cancel the given task after a moment

async def cancel_task(task):

    # block for a moment

    await asyncio.sleep(0.2)

    # cancel the task

    was_cancelled = task.cancel()

    print(f'cancelled: {was_cancelled}')

# define a simple coroutine

async def main():

    # create the coroutine

    coro = simple_task(1)

    # created the shielded task

    shielded = asyncio.shield(coro)

    # create the task to cancel the previous task

    asyncio.create_task(cancel_task(shielded))

    # handle cancellation

    try:

        # await the shielded task

        result = await shielded

        # report the result

        print(f'>got: {result}')

    except asyncio.CancelledError:

        print('shielded was cancelled')

    # get all tasks

    tasks = asyncio.all_tasks()

    # wait a moment

    await asyncio.sleep(1)

    # report the details of the tasks

    print(f'shielded: {shielded}')

    # report the task for the coroutine

    for task in tasks:

        if task.get_coro() is coro:

            print(f'task: {task}')

# start

asyncio.run(main())

Running the example first creates the main() coroutine and uses it as the entry point into the application.

The task coroutine is created. It is then shielded from cancellation. Internally this wraps the coroutine in a Task object and schedules it for execution. We will retrieve this Task object later.

The shielded task is then passed to the cancel_task() coroutine which is wrapped in a task and scheduled.

The main coroutine then awaits the shielded task, which expects a CancelledError exception.

The task runs for a moment then sleeps. The cancellation task runs for a moment, sleeps, resumes, then cancels the shielded task. The request to cancel reports that it was successful.

This raises a CancelledError exception in the shielded Future, although not in the inner task or coroutine.

The main() coroutine resumes and responds to the CancelledError exception, reporting a message. It then sleeps for a while longer.

The inner task resumes, finishes, and returns a value.

Finally, the main() coroutine resumes, reporting the status of the shielded future. It then locates the Task associated with the inner coroutine and reports its status.

We can see that the shielded future is marked as canceled and yet the inner task for the shielded coroutine is marked as finished normally and provides a return value.

This example highlights how a shield can be used to successfully protect an inner coroutine from cancellation, and that the shield() function will create an asyncio.Task object for a provided coroutine.

cancelled: True

shielded was cancelled

shielded: <Future cancelled>

task: <Task finished name='Task-2' coro=<simple_task() done, defined at ...> result=1>

Next, let’s look at what happens when we cancel the task with a shield.

Example of Canceling Shielded Task

We can explore what happens when the inner shielded task is canceled.

In this example, we will pass the inner task to the cancellation coroutine.

The expectation is that the inner task will be canceled and that this request for cancellation will be propagated out to the shield and then impact the main coroutine.

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

# SuperFastPython.com

# example of canceling the task directly inside the shield

import asyncio

# define a simple asynchronous

async def simple_task(number):

    # block for a moment

    await asyncio.sleep(1)

    # return the argument

    return number

# cancel the given task after a moment

async def cancel_task(task):

    # block for a moment

    await asyncio.sleep(0.2)

    # cancel the task

    was_cancelled = task.cancel()

    print(f'cancelled: {was_cancelled}')

# define a simple coroutine

async def main():

    # create the coroutine

    coro = simple_task(1)

    # create a task

    task = asyncio.create_task(coro)

    # created the shielded task

    shielded = asyncio.shield(task)

    # create the task to cancel the previous task

    asyncio.create_task(cancel_task(task))

    # handle cancellation

    try:

        # await the shielded task

        result = await shielded

        # report the result

        print(f'>got: {result}')

    except asyncio.CancelledError:

        print('shielded was cancelled')

    # wait a moment

    await asyncio.sleep(1)

    # report the details of the tasks

    print(f'shielded: {shielded}')

    print(f'task: {task}')

# start

asyncio.run(main())

Running the example first creates the main() coroutine and uses it as the entry point into the application.

The task coroutine is created, then it is wrapped and scheduled in a Task.

The task is then shielded from cancellation.

The task itself, not the shielded task, is then passed to the cancel_task() coroutine that is wrapped in a task and scheduled.

The main coroutine then awaits the shielded task, which expects a CancelledError exception.

The task runs for a moment then sleeps. The cancellation task runs for a moment, sleeps, resumes then cancels the task directly. The request to cancel reports that it was successful.

This raises a CancelledError exception in the task itself which cancels. The CancelledError exception is then raised in the shielded Future object, which also cancels.

The main() coroutine resumes and responds to the CancelledError exception, reporting a message. It then sleeps for a while longer.

Finally, the main() coroutine resumes, and reports the status of the shielded future and the inner task.

We can see that both the shielded Future object and the inner Task are marked as canceled.

This example highlights that although a Task can be shielded, it can still be canceled directly.

cancelled: True

shielded was cancelled

shielded: <Future cancelled>

task: <Task cancelled name='Task-2' coro=<simple_task() done, defined at ...>>


Python Asyncio Jump-Start

Loving The Tutorials?

Why not take the next step? Get the book.

Learn more
 


Example of Asyncio shield() with wait_for()

We can explore what happens to a shielded task that is canceled by a call to wait_for() after a timeout.

In this example, we shield the task from cancellation as before. In this case, we pass the shielded Future to a call to the asyncio.wait_for() function.

This function will wait for a task to complete with a timeout. If the timeout elapses before the task is complete, it is canceled.

You can learn more about the wait_for() function in the tutorial:

The expectation is that the shielded future will be canceled after the timeout by the wait_for() function, although this will not impact the internal task.

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

# SuperFastPython.com

# example of using wait_for() with a shielded task

import asyncio

# define a simple asynchronous

async def simple_task(number):

    # block for a moment

    await asyncio.sleep(1)

    # return the argument

    return number

# define a simple coroutine

async def main():

    # create the coroutine

    coro = simple_task(1)

    # create a task

    task = asyncio.create_task(coro)

    # created the shielded task

    shielded = asyncio.shield(task)

    # execute shielded task with a timeout

    try:

        # await the shielded task

        result = await asyncio.wait_for(shielded, timeout=0.2)

        # report the result

        print(f'>got: {result}')

    except asyncio.CancelledError:

        print('wait_for was cancelled')

    except asyncio.TimeoutError:

        print('timed out waiting for result')

    # wait a moment

    await asyncio.sleep(1)

    # report the details of the tasks

    print(f'shielded: {shielded}')

    print(f'task: {task}')

# start

asyncio.run(main())

Running the example first creates the main() coroutine and uses it as the entry point into the application.

The task coroutine is created, then it is wrapped and scheduled in a Task.

The task is then shielded from cancellation.

The shielded task is then passed to the wait_for() function and a timeout of a fraction of a second is used. This call expects a possible CancelledError exception if the shield is canceled and a TimeoutError if the task is canceled with a timeout.

The main coroutine then awaits the wait_for() task.

The task runs for a moment then sleeps. The timeout in the wait_for() task elapses and cancels the shielded Future.

This raises a TimeoutError exception in the shielded Future, although not in the inner task.

The main() coroutine resumes and responds to the TimeoutError exception, reporting a message. It then sleeps for a while longer.

The task resumes, finishes, and returns a value.

Finally, the main() coroutine resumes, and reports the status of the shielded future and the inner task. We can see that the shielded future is marked as canceled and yet the inner task is marked as finished normally and provides a return value.

This example highlights how the shield can be used to protect a time from cancellation due to a timeout when using the asyncio.wait_for() function.

timed out waiting for result

shielded: <Future cancelled>

task: <Task finished name='Task-2' coro=<simple_task() done, defined at ...> result=1>

Common Questions

This section considers common questions related to shielding tasks from cancellation.

Do you have any questions about shielding tasks from cancellation?
Ask your questions in the comments below and I will do my best to answer them and may add them to this section.

Can a Task Be Shielded?

Yes.

Can a Coroutine Be Shielded?

Yes.

A coroutine passed to shield() will be wrapped in an asyncio.Task and scheduled immediately.

The Future returned from shield() does not need to be awaited in order for the provided coroutine to be executed.

How Can We Wait For a Shielded Task To Complete?

We can wait for a shielded task to complete by keeping a reference to the asyncio.Task object that was shielded and waiting for it directly.

Alternatively, if a coroutine was provided to the shield() function, then the associated asyncio.Task can be located from asyncio.all_tasks() and awaited.

How Can We Get The Result from a Canceled Shielded Task?

We can get the result from a canceled shielded task by keeping a reference to the asyncio.Task object that was shielded and called the result() method in order to get the return value.

Alternatively, if a coroutine was provided to the shield() function, then the associated asyncio.Task can be located from asyncio.all_tasks() and the result() method can be called on it.

What Happens if The Shielded Task Itself is Canceled?

If the inner task that is being shielded is canceled, it will cancel the external Future that wraps it.

This means that cancellations are propagated up from the inner task to the outer shield task.

Can the Inner Task Still Be Canceled?

Yes.

If the shielded inner asyncio.Task object is canceled directly, then the task will raise an asyncio.CancelledError exception and can cancel the task.

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 shield asyncio tasks from cancellation in Python.

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

Photo by Aaron Huber on Unsplash