You can cancel a request to cancel a task by developing asyncio tasks that are robust to cancellation.
This requires tasks that consume a raised CancelledError on cancellation and reset their own count of cancellation requests.
In this tutorial, you will discover how to cancel an asyncio task cancellation (uncancel a task).
Let’s get started.
What is Asyncio Task Cancellation?
Asyncio tasks can be canceled.
This can be achieved by calling the cancel() method on the asyncio.Task. This will request that the task be canceled as soon as possible and return True if the request was successful or False if it was not, e.g. the task is already done.
Request the Task to be cancelled. This arranges for a CancelledError exception to be thrown into the wrapped coroutine on the next cycle of the event loop.
— Coroutines and Tasks
For example:
1 2 3 4 |
... # cancel a task if task.cancel(): # request successful |
The next time that the target task that was cancelled resumes, a CancelledError exception will be raised.
This exception will bubble up to the top level of the task and cause it to stop running.
You can learn more about the mechanics of cancellation a task in the tutorial:
Next, let’s consider the need to cancel a request to cancel a task.
Run loops using all CPUs, download your FREE book to learn how.
Need to Cancel Task Cancellation
We may cancel a task, then later decide that we do not want to cancel it.
We want to cancel the task cancellation, or put it another way, we want to uncancel a task.
This may be for many reasons, such as the condition that triggered cancellation changed.
Canceling a cancellation request will allow the task to have the external state of not having been canceled.
It will also have the internal state of (nominally) continuing execution of the task body.
How can we cancel a task cancellation request?
Canceling Task Cancellation is Not Shielding
Canceling a task cancellation request is different from shielding a task from cancellation.
Recall that we can shield a task from cancellation by wrapping it in another task that consumes the CancelledError exception.
This can be achieved via the asyncio.shield() call.
For example:
1 2 3 4 5 |
... # shield task shielded = asyncio.shield(task()) # cancel shielded task shielded.cancel() |
You can learn more about shielding tasks from cancellation in the tutorial:
Instead, we are concerned with the inner task that we know will receive the cancellation request directly.
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.
How to Cancel Task Cancellation
A task is cancelled if:
- The cancel() method is called on the asyncio.Task object.
- The cancelling() method now returns an integer of 1 or greater.
- An asyncio.CancelledError exception is raised within the task body.
- The asyncio.CancelledError exception bubbles up causing the task to be done.
- The cancelled() method returns True, once the task is done.
Given this process of how cancellation occurs, there are perhaps two considerations when canceling task cancellation, they are:
- Removing the request to cancel so that cancelling() returns 0.
- Handling or ignoring the asyncio.CancelledError exception.
Let’s consider both in turn.
Removing the Request to Cancel with Task.uncancel()
An asyncio.Task keeps track of the number of times it is requested to cancel.
This feature was added in Python 3.11.
Added cancelling() and uncancel() methods to Task. These are primarily intended for internal use, notably by TaskGroup.
— What’s New In Python 3.11
Each call to the Task.cancel() method increments the count.
The Task.cancelling() method returns the count.
Return the number of pending cancellation requests to this Task, i.e., the number of calls to cancel() less the number of uncancel() calls.
— Coroutines and Tasks
The Task.uncancel() decrements the count.
Decrement the count of cancellation requests to this Task.
— Coroutines and Tasks
- Task.cancel(): Increment cancel requests.
- Task.cancelling(): Return cancel requests.
- Task.uncancel(): Decrement cancel requests.
An intuition is that a request to cancel is acted upon by the task the next time it resumes only if the cancelling() method returns an integer of 1 or more.
This is not true.
For example, calling cancel() followed by uncancel() on a task and then awaiting the task will still result in the task internally raising an asyncio.CancelledError and being cancelling.
For example:
1 2 3 4 5 6 7 |
... # cancel the task task.cancel() # decide against the cancellation task.uncancel() # await the task await task # raises a asyncio.CancelledError |
Therefore, it is a good practice to decrement the requests for cancellation with a call to the uncancel() method if we no longer want the task to cancel, but it is not sufficient to cancel the cancellation.
If end-user code is, for some reason, suppressing cancellation by catching CancelledError, it needs to call this method to remove the cancellation state.
— Coroutines and Tasks
Handle and Ignore CancelledError
Cancelling a task cancellation request must involve the handing of the CancelledError raised in the target task.
This may be achieved by wrapping the task in a task that consumes the CancelledError, such as via asyncio.shield(), but this may limit direct access to the underlying task so that we can call uncancel() and reset the cancellation request count.
Alternatively, we can handle the CancelledError directly within the body of the task.
At a high level, this might look like the following:
1 2 3 4 5 6 7 8 9 |
# custom coroutine async def work(): # handle cancellation try: # do work # ... except asyncio.CancelledError: # ignore pass |
More robustly, each point of suspension in a task would require such wrapping, as those are the points where the task may be suspended, and then resume with a raised asyncio.CancelledError.
The consumption of the CancelledError exceptions could be conditional, enabled or disabled internally or externally using a variable attribute.
Additionally, the request for cancellation should also be scrubbed from the task.
This can be achieved by calling the uncancel() method on the task externally.
Internally, this can be achieved by calling asyncio.current_task() to get the current asyncio.Task object, then calling the uncancel() method.
For example:
1 2 3 |
... # reset cancelling counter asyncio.current_task().uncancel() |
Both consume the asyncio.CancelledError and resetting the number of requested cancellations will allow the asyncio.Task to look, feel, and act like an uncancelled task.
Note that consuming asyncio.CancelledError exceptions is discouraged.
See the tutorial:
Now that we know how to cancel a request to cancel a task, let’s look at some worked examples.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example of uncancel() Does Not Prevent a CancelledError Being Raised
We can explore an example of uncancel() not preventing an CancelledError exception being raised in the cancelled task.
In this example, we will define a task that takes a while to run. The task will be scheduled and given time to run. The task will then be canceled via a call to cancel(), then uncancelled via a call to uncancel(), then awaited.
The expectation is that the target task will be cancelled by raising a CancelledError exception, and that awaiting the target task will cause the CancelledError to bubble up to the top level and terminate the event loop.
Firstly, we can define the long-running task that sleeps for 5 seconds and reports a message.
1 2 3 4 5 6 |
# task that take a long time async def work(): # sleep a long time await asyncio.sleep(5) # report a message print('Task sleep completed normally') |
Next, the main() coroutine can be defined that creates and schedules the work() task and allows it to begin executing.
It then cancels the task, reports the number of cancel requests and uncancelled the task, and again reports the number of cancel requests to confirm it is reset. The task is then awaited.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# main coroutine async def main(): # create and schedule the task task = asyncio.create_task(work()) # allow the task to run a moment await asyncio.sleep(1) # cancel the task task.cancel() print(f'Task Cancelling: {task.cancelling()}') # decide against the cancellation task.uncancel() print(f'Task Cancelling: {task.cancelling()}') # await the task await task |
Finally, the event loop is started.
1 2 3 |
... # start the event loop asyncio.run(main()) |
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 |
# SuperFastPython.com # example of canceling a task cancellation within the task import asyncio # task that take a long time async def work(): # sleep a long time await asyncio.sleep(5) # report a message print('Task sleep completed normally') # main coroutine async def main(): # create and schedule the task task = asyncio.create_task(work()) # allow the task to run a moment await asyncio.sleep(1) # cancel the task task.cancel() print(f'Task Cancelling: {task.cancelling()}') # decide against the cancellation task.uncancel() print(f'Task Cancelling: {task.cancelling()}') # await the task await task # start the event loop asyncio.run(main()) |
Running the example first starts the event loop.
The main() coroutine runs and creates and schedules the work() task.
The main() coroutine suspends and allows the work() task to run.
The work() task runs and suspends on a call to sleep.
The main() coroutine resumes and cancels the work() task, then reports the number of cancellation requests.
This shows the one expected request.
The main() coroutine then uncancels the cancel request on the work() task and again reports the number of cancellation requests.
This shows zero requests, as expected, confirming the count has been reset.
The main() coroutine then suspends and awaits the work() coroutine.
The work() coroutine resumes and raises a CancelledError exception, canceling the task.
This bubbles up to main() and is not handled, causing the event loop to be terminated.
This highlights that calling uncancel() on a task is not sufficient to cancel a request for cancellation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Task Cancelling: 1 Task Cancelling: 0 Traceback (most recent call last): File "..", line 32, in <module> asyncio.run(main()) File ".../asyncio/runners.py", line 190, in run return runner.run(main) ^^^^^^^^^^^^^^^^ File ".../asyncio/runners.py", line 118, in run return self._loop.run_until_complete(task) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../asyncio/base_events.py", line 653, in run_until_complete return future.result() ^^^^^^^^^^^^^^^ File ...", line 25, in main await task File "...", line 8, in work await asyncio.sleep(5) File ".../asyncio/tasks.py", line 639, in sleep return await future ^^^^^^^^^^^^ asyncio.exceptions.CancelledError |
Next, let’s explore an example of actively canceling or suppressing a request to cancel a task.
Example of Cancelling Task Cancellation From Within The Task
We can explore a case of actively canceling a request to cancel an asyncio task.
This requires both consuming the raised CancelledError within the task and resetting the cancellation request count.
In this case, we will develop a contrived task that is robust to cancellation. Each time it is cancelled, it will attempt to retry the same task from scratch. When cancelled it will handle and consume the CancelledError and reset its own internal cancellation request count.
The work() coroutine below implements this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# task that take a long time async def work(): # ensure the task always gets done while True: try: # sleep a long time await asyncio.sleep(5) # report a message print('Task sleep completed normally') # stop, we are done break except asyncio.CancelledError: # report a message print('Task as cancelled, ignore and trying again') # reset cancelling counter asyncio.current_task().uncancel() |
This suggests that the lengths required to ensure a task is robust to cancellation, e.g. allowing restarts potentially with checkpoints. Onerous to say the least.
The main() coroutine will then create and schedule the task and allow it to begin executing.
1 2 3 4 5 |
... # create and schedule the task task = asyncio.create_task(work()) # allow the task to run a moment await asyncio.sleep(1) |
Next, the main() coroutine will cancel the task and await it, expecting it to raise a CancelledError, which we know it will not.
1 2 3 4 5 6 7 8 |
... # cancel the task task.cancel() # await the task try: await task except asyncio.CancelledError: print('Main saw the task cancel') |
Finally, the details of the task are reported.
1 2 3 4 5 6 |
... # report details of the task print(f'Task Cancelling: {task.cancelling()}') print(f'Task Cancelled: {task.cancelled()}') print(f'Task: {task}') print('Main is done') |
The complete main() coroutine is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# main coroutine async def main(): # create and schedule the task task = asyncio.create_task(work()) # allow the task to run a moment await asyncio.sleep(1) # cancel the task task.cancel() # await the task try: await task except asyncio.CancelledError: print('Main saw the task cancel') # report details of the task print(f'Task Cancelling: {task.cancelling()}') print(f'Task Cancelled: {task.cancelled()}') print(f'Task: {task}') print('Main is done') |
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 |
# SuperFastPython.com # example of canceling a task cancellation within the task import asyncio # task that take a long time async def work(): # ensure the task always gets done while True: try: # sleep a long time await asyncio.sleep(5) # report a message print('Task sleep completed normally') # stop, we are done break except asyncio.CancelledError: # report a message print('Task as cancelled, ignore and trying again') # reset cancelling counter asyncio.current_task().uncancel() # main coroutine async def main(): # create and schedule the task task = asyncio.create_task(work()) # allow the task to run a moment await asyncio.sleep(1) # cancel the task task.cancel() # await the task try: await task except asyncio.CancelledError: print('Main saw the task cancel') # report details of the task print(f'Task Cancelling: {task.cancelling()}') print(f'Task Cancelled: {task.cancelled()}') print(f'Task: {task}') print('Main is done') # start the event loop asyncio.run(main()) |
Running the example first starts the event loop.
The main() coroutine runs and creates and schedules the work() task.
The main() coroutine suspends and allows the work() task to run.
The work() task runs and suspends on a call to sleep.
The main() coroutine resumes and cancels the work() task, then awaits the task to be cancelled.
The work() task raises a CancelledError, which is handled. A message is logged, and the task resets its cancel request count. It then restarts the task.
The work() task completes normally and reports a message, then terminates.
The main() coroutine resumes and does not handle a CancelledError, because it was not propagated up from the task.
The main() coroutine then reports the details of the task, highlighting that the cancel request count is zero and that task was not cancelled.
Report the task itself, we can see that it has the state “done” and not “cancelled“.
This highlights how we might effectively uncancel a cancelled task from within the task, by making the task robust to requests for cancellation.
1 2 3 4 5 6 |
Task as cancelled, ignore and trying again Task sleep completed normally Task Cancelling: 0 Task Cancelled: False Task: <Task finished name='Task-2' coro=<work() done, defined at ...> result=None> Main is done |
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 to cancel an asyncio task cancellation (uncancel a task).
Did I make a mistake? See a typo?
I’m a simple humble human. Correct me, please!
Do you have any additional tips?
I’d love to hear about them!
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Şahin Sezer Dinçer on Unsplash
Do you have any questions?