Cancellation and timeouts — AnyIO 4.9.0 documentation (2025)

The ability to cancel tasks is the foremost advantage of the asynchronous programmingmodel. Threads, on the other hand, cannot be forcibly killed and shutting them down willrequire perfect cooperation from the code running in them.

Cancellation in AnyIO follows the model established by the Trio framework. This meansthat cancellation of tasks is done via so called cancel scopes. Cancel scopes are usedas context managers and can be nested. Cancelling a cancel scope cancels all cancelscopes nested within it. If a task is waiting on something, it is cancelled immediately.If the task is just starting, it will run until it first tries to run an operationrequiring waiting, such as sleep().

A task group contains its own cancel scope. The entire task group can be cancelled bycancelling this scope.

Differences between asyncio and AnyIO cancellation semantics

Asyncio employs a type of cancellation called edge cancellation. This means that whena task is cancelled, a CancelledError is raised in the task and the taskthen gets to handle it however it likes, even opting to ignore it entirely. In contrast,tasks that either explicitly use a cancel scope, or are spawned from an AnyIO taskgroup, use level cancellation. This means that as long as a task remains within aneffectively cancelled cancel scope, it will get hit with a cancellation exception anytime it hits a yield point (usually by awaiting something, or throughasync with ... or async for ...).

This can cause difficulties when running code written for asyncio that does not expectto get cancelled repeatedly. For example, asyncio.Condition was written in sucha way that it suppresses cancellation exceptions until it is able to reacquire theunderlying lock. This can lead to a busy-wait loop that needlessly consumes a lot ofCPU time.

Timeouts

Networked operations can often take a long time, and you usually want to set up somekind of a timeout to ensure that your application doesn’t stall forever. There are twoprincipal ways to do this: move_on_after() and fail_after(). Both are usedas synchronous context managers. The difference between these two is that the formersimply exits the context block prematurely on a timeout, while the other raises aTimeoutError.

Both methods create a new cancel scope, and you can check the deadline by accessing thedeadline attribute. Note, however, that an outer cancel scopemay have an earlier deadline than your current cancel scope. To check the actualdeadline, you can use the current_effective_deadline() function.

Here’s how you typically use timeouts:

from anyio import create_task_group, move_on_after, sleep, runasync def main(): async with create_task_group() as tg: with move_on_after(1) as scope: print('Starting sleep') await sleep(2) print('This should never be printed') # The cancelled_caught property will be True if timeout was reached print('Exited cancel scope, cancelled =', scope.cancelled_caught)run(main)

Note

It’s recommended not to directly cancel a scope from fail_after(), asthat may currently result in TimeoutError being erroneously raised if exitingthe scope is delayed long enough for the deadline to be exceeded.

Shielding

There are cases where you want to shield your task from cancellation, at leasttemporarily. The most important such use case is performing shutdown procedures onasynchronous resources.

To accomplish this, open a new cancel scope with the shield=True argument:

from anyio import CancelScope, create_task_group, sleep, runasync def external_task(): print('Started sleeping in the external task') await sleep(1) print('This line should never be seen')async def main(): async with create_task_group() as tg: with CancelScope(shield=True) as scope: tg.start_soon(external_task) tg.cancel_scope.cancel() print('Started sleeping in the host task') await sleep(1) print('Finished sleeping in the host task')run(main)

The shielded block will be exempt from cancellation except when the shielded blockitself is being cancelled. Shielding a cancel scope is often best combined withmove_on_after() or fail_after(), both of which also acceptshield=True.

Finalization

Sometimes you may want to perform cleanup operations in response to the failure of theoperation:

async def do_something(): try: await run_async_stuff() except BaseException: # (perform cleanup) raise

In some specific cases, you might only want to catch the cancellation exception. This istricky because each async framework has its own exception class for that and AnyIOcannot control which exception is raised in the task when it’s cancelled. To work aroundthat, AnyIO provides a way to retrieve the exception class specific to the currentlyrunning async framework, using get_cancelled_exc_class():

from anyio import get_cancelled_exc_classasync def do_something(): try: await run_async_stuff() except get_cancelled_exc_class(): # (perform cleanup) raise

Warning

Always reraise the cancellation exception if you catch it. Failing to do somay cause undefined behavior in your application.

If you need to use await during finalization, you need to enclose it in a shieldedcancel scope, or the operation will be cancelled immediately since it’s in an alreadycancelled scope:

async def do_something(): try: await run_async_stuff() except get_cancelled_exc_class(): with CancelScope(shield=True): await some_cleanup_function() raise

Avoiding cancel scope stack corruption

When using cancel scopes, it is important that they are entered and exited in LIFO (lastin, first out) order within each task. This is usually not an issue since cancel scopesare normally used as context managers. However, in certain situations, cancel scopestack corruption might still occur:

  • Manually calling CancelScope.__enter__() and CancelScope.__exit__(), usuallyfrom another context manager class, in the wrong order

  • Using cancel scopes with [Async]ExitStack in a manner that couldn’t be achieved bynesting them as context managers

  • Using the low level coroutine protocol to execute parts of the coroutine function indifferent cancel scopes

  • Yielding in an async generator while enclosed in a cancel scope

Remember that task groups contain their own cancel scopes so the same list of riskysituations applies to them too.

As an example, the following code is highly dubious:

# Bad!async def some_generator(): async with create_task_group() as tg: tg.start_soon(foo) yield

The problem with this code is that it violates structural concurrency: what happens ifthe spawned task raises an exception? The host task would be cancelled as a result, butthe host task might be long gone by the time that happens. Even if it weren’t, anyenclosing try...except in the generator would not be triggered. Unfortunately thereis currently no way to automatically detect this condition in AnyIO, so in practice youmay simply experience some weird behavior in your application as a consequence ofrunning code like above.

Depending on how they are used, this pattern is, however, usually safe to use inasynchronous context managers, so long as you make sure that the same host task keepsrunning throughout the entire enclosed code block:

# Okay in most cases!@async_context_managerasync def some_context_manager(): async with create_task_group() as tg: tg.start_soon(foo) yield

Prior to AnyIO 3.6, this usage pattern was also invalid in pytest’s asynchronousgenerator fixtures. Starting from 3.6, however, each async generator fixture is run fromstart to end in the same task, making it possible to have task groups or cancel scopessafely straddle the yield.

When you’re implementing the async context manager protocol manually and your asynccontext manager needs to use other context managers, you may find it necessary to calltheir __aenter__() and __aexit__() directly. In such cases, it is absolutelyvital to ensure that their __aexit__() methods are called in the exact reverse orderof the __aenter__() calls. To this end, you may find theAsyncExitStack class very useful:

from contextlib import AsyncExitStackfrom anyio import create_task_groupclass MyAsyncContextManager: async def __aenter__(self): self._exitstack = AsyncExitStack() await self._exitstack.__aenter__() self._task_group = await self._exitstack.enter_async_context( create_task_group() ) async def __aexit__(self, exc_type, exc_val, exc_tb): return await self._exitstack.__aexit__(exc_type, exc_val, exc_tb)
Cancellation and timeouts — AnyIO 4.9.0 documentation (2025)

References

Top Articles
Latest Posts
Recommended Articles
Article information

Author: Carlyn Walter

Last Updated:

Views: 6069

Rating: 5 / 5 (50 voted)

Reviews: 81% of readers found this page helpful

Author information

Name: Carlyn Walter

Birthday: 1996-01-03

Address: Suite 452 40815 Denyse Extensions, Sengermouth, OR 42374

Phone: +8501809515404

Job: Manufacturing Technician

Hobby: Table tennis, Archery, Vacation, Metal detecting, Yo-yoing, Crocheting, Creative writing

Introduction: My name is Carlyn Walter, I am a lively, glamorous, healthy, clean, powerful, calm, combative person who loves writing and wants to share my knowledge and understanding with you.