Asyncio Task That Cancels Itself

December 28, 2023 Python Asyncio

You can develop a task that cancels itself by first getting access to the asyncio.Task instance using asyncio.current_task(), then calling the cancel() method.

In this tutorial, you will discover how an asyncio task can cancel itself in Python.

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:

...
# cancel a task
if task.cancel():
	# request successful

The next time that the target task that was canceled 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:

You can also learn more about task cancellation best practices in the tutorial:

Task Needs to Cancel Itself

Sometimes we may need a task to cancel itself.

This may be for many reasons.

Most commonly, some exceptional events may have occurred. Instead of propagating the exception we want the task to be canceled, and importantly, to have the "cancelled" state when inspected externally.

Alternatively, a complex task may catch and handle a CancelledError exception, then not want to propagate the CancelledError until the next time it resumes.

Can a task cancel itself?

How can we implement a task that cancels itself?

Asyncio Tasks Can Cancel Itself

An asyncio task can cancel itself.

This requires that the task has or retrieves a reference to its own asyncio.Task instance then calls the cancel() method.

The next time the task is suspended, a CancelledError will be raised.

If the task does not suspend, then a CancelledError will be raised when it terminates.

Any subtasks of the current task will also be canceled automatically, as long as they are being awaited by the task that canceled itself.

A task can get access to its own asyncio.Task instance via the asyncio.current_task() function.

For example:

...
# get the current task
task = asyncio.current_task()

You can learn more about getting the current task in the tutorial:

We can then cancel the task.

...
# cancel the current task
task.cancel()

Or, in one line:

...
# cancel the current task
asyncio.current_task().cancel()

And that's all there is to it.

Next, let's look at some examples of tasks canceling themselves.

Example of Asyncio Task Canceling Itself

We can explore an example of a task that cancels itself which takes effect the next time it suspends.

In this example, we will create a task and await it in the main coroutine. The task will report a message, sleep, and then cancel itself. It will then sleep again which will cause the CancelledError exception to be propagated and observed in the main coroutine.

Firstly, we can define the task that cancels itself.

The task reports a message, sleeps, cancels itself, sleeps again, and then reports a final message.

The work() coroutine below implements this.

# task that take a long time
async def work():
    # report a message
    print('Task is starting')
    # block a moment
    await asyncio.sleep(0.5)
    # task cancels itself
    asyncio.current_task().cancel()
    # block a moment
    await asyncio.sleep(0.5)
    # report a message
    print('Task is done')

We don't expect the final message to be reported as the CancelledError will be raised and propagate on the second sleep.

Next, the main() coroutine can be defined that creates the work() task in the background.

It then awaits it within a try-except block and checks for a CancelledError exception.

# main coroutine
async def main():
    # create and schedule the task
    task = asyncio.create_task(work())
    # await the task
    try:
        await task
    except asyncio.CancelledError:
        print('Main saw the task cancel')
    print('Main is done')

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of an asyncio task canceling itself
import asyncio

# task that take a long time
async def work():
    # report a message
    print('Task is starting')
    # block a moment
    await asyncio.sleep(0.5)
    # task cancels itself
    asyncio.current_task().cancel()
    # block a moment
    await asyncio.sleep(0.5)
    # report a message
    print('Task is done')

# main coroutine
async def main():
    # create and schedule the task
    task = asyncio.create_task(work())
    # await the task
    try:
        await task
    except asyncio.CancelledError:
        print('Main saw the task cancel')
    print('Main is done')

# start the event loop
asyncio.run(main())

Running the example first creates and runs the main() coroutine.

The main() coroutine creates and runs the work() coroutine in the background, and then awaits it.

The work() coroutine runs and reports a message then suspends with a sleep.

The work() coroutine resumes and cancels itself. It then suspends again with a second sleep.

The CancelledError is raised and bubbles up, terminating the work() coroutine.

The main() coroutine resumes and handles the CancelledError reporting a cancellation message, and then reports the final message.

This highlights how a task can cancel itself and how the CancelledError is not raised until the next time the task is suspended.

Task is starting
Main saw the task cancel
Main is done

Example of Task Canceling Itself on Termination

We can explore an example of a task that cancels itself that takes effect when the task is terminated.

In this case, we will update the above example so that it does not explicitly sleep after canceling itself.

The updated work() coroutine with this change is listed below.

# task that take a long time
async def work():
    # report a message
    print('Task is starting')
    # task cancels itself
    asyncio.current_task().cancel()
    # report a message
    print('Task is done')

This will prevent the CancelledError from rising in the work() coroutine while running.

Instead, the CancelledError will propagate after the work() coroutine terminates.

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of an asyncio task canceling itself on termination
import asyncio

# task that take a long time
async def work():
    # report a message
    print('Task is starting')
    # task cancels itself
    asyncio.current_task().cancel()
    # report a message
    print('Task is done')

# main coroutine
async def main():
    # create and schedule the task
    task = asyncio.create_task(work())
    # await the task
    try:
        await task
    except asyncio.CancelledError:
        print('Main saw the task cancel')
    print('Main is done')

# start the event loop
asyncio.run(main())

Running the example first creates and runs the main() coroutine.

The main() coroutine creates and runs the work() coroutine in the background, and then awaits it.

The work() coroutine runs and reports a message then suspends with a sleep.

The work() coroutine resumes and cancels itself. It then reports a second message and then terminates.

Once the work() coroutine is done, it raises a CancelledError exception, which propagates up to the parent task.

The main() coroutine resumes and handles the CancelledError reporting a cancellation message, and then reports the final message.

This highlights how a task can cancel itself and the CancelledError exception is not propagated until it terminates.

Task is starting
Task is done
Main saw the task cancel
Main is done

Example of Task With Subtask Canceling Itself

We can explore an example of a task that cancels itself and also cancels a subtask.

In this case, we will update the above example so that the work() coroutine creates a long-running coroutine to run in the background.

It will then cancel itself and sleep, allowing the CancelledError to be raised and propagate, terminating itself and its subtask.

Firstly, we can define the subtask. This task will sleep for 10 seconds and handles the CancelledError exception in order to report a message, so we can see when it is canceled, then re-raise the CancelledError exception.

The work2() coroutine below implements this.

# task that take a long time
async def work2():
    try:
        # sleep a long time
        await asyncio.sleep(10)
        # report a normal message
        print('Subtask sleep completed normally')
    except asyncio.CancelledError:
        # report a message
        print('Subtask is cancelled')
        # re-raise the cancellation
        raise

Next, we can update the work() coroutine to create and issue the work2() coroutine in the background.

It will then await the work2() task after canceling itself, allowing the CancelledError to propagate down to the subtask.

# task that take a long time
async def work():
    # report a message
    print('Task is starting')
    # issue a long running task
    task = asyncio.create_task(work2())
    # block a moment
    await asyncio.sleep(0.5)
    # task cancels itself
    asyncio.current_task().cancel()
    # wait for the child task to complete
    await task
    # report a message
    print('Task is done')

Tying this together, the complete example is listed below.

# SuperFastPython.com
# example of an asyncio task with a subtask canceling itself
import asyncio

# task that take a long time
async def work2():
    try:
        # sleep a long time
        await asyncio.sleep(10)
        # report a normal message
        print('Subtask sleep completed normally')
    except asyncio.CancelledError:
        # report a message
        print('Subtask is cancelled')
        # re-raise the cancellation
        raise

# task that take a long time
async def work():
    # report a message
    print('Task is starting')
    # issue a long running task
    task = asyncio.create_task(work2())
    # block a moment
    await asyncio.sleep(0.5)
    # task cancels itself
    asyncio.current_task().cancel()
    # wait for the child task to complete
    await task
    # report a message
    print('Task is done')

# main coroutine
async def main():
    # create and schedule the task
    task = asyncio.create_task(work())
    # await the task
    try:
        await task
    except asyncio.CancelledError:
        print('Main saw the task cancel')
    print('Main is done')

# start the event loop
asyncio.run(main())

Running the example first creates and runs the main() coroutine.

The main() coroutine creates and runs the work() coroutine in the background, and then awaits it.

The work() coroutine runs and reports a message. It then creates and schedules the work2() coroutine before suspending with a sleep.

The work2() coroutine runs and suspends with a sleep.

The CancelledError first propagates down and cancels the awaited subtask. The CancelledError exception is raised in the work2() coroutine, which is handled, reporting a message, then re-raised.

The CancelledError exception bubbles up terminating the work() coroutine.

The main() coroutine resumes and handles the CancelledError reporting a cancellation message, and then reports the final message.

This highlights how a task can cancel itself and cancel its subtasks if the subtask is explicitly awaited after the request to self-cancel.

Note, if we did not await work2() in work() and instead left it to run in the background, then it would be free to resume or terminate (be canceled) when the event loop is terminated.

Task is starting
Subtask is cancelled
Main saw the task cancel
Main is done

Takeaways

You now know how an asyncio task can cancel itself in Python.



If you enjoyed this tutorial, you will love my book: Python Asyncio Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.