Skip to content

Add asyncio.CancelScope with level-triggered cancellation#145345

Closed
kovan wants to merge 2 commits intopython:mainfrom
kovan:asyncio-cancel-scope
Closed

Add asyncio.CancelScope with level-triggered cancellation#145345
kovan wants to merge 2 commits intopython:mainfrom
kovan:asyncio-cancel-scope

Conversation

@kovan
Copy link
Contributor

@kovan kovan commented Feb 28, 2026

Summary

  • Add asyncio.CancelScope, an async context manager providing level-triggered cancellation for asyncio
  • Once cancelled (via cancel() or deadline expiry), every subsequent await inside the scope raises CancelledError until the scope exits — the coroutine cannot simply catch-and-ignore
  • Implements both Python (_PyTask) and C (_CTask) Task changes
  • Existing edge-triggered mechanisms (cancel()/uncancel(), Timeout, TaskGroup) remain completely unchanged; code not using CancelScope has zero overhead

Motivated by the discuss.python.org proposal to adopt proven Trio/anyio patterns into asyncio.

New public API

  • asyncio.CancelScope(*, deadline=None, shield=False) — async context manager
  • asyncio.cancel_scope(delay, *, shield=False) — convenience with relative delay
  • asyncio.cancel_scope_at(when, *, shield=False) — convenience with absolute time

Properties: deadline (r/w), shield (r/w), cancel_called (r), cancelled_caught (r)
Methods: cancel(), reschedule(deadline)

How it works

CancelScope pushes/pops itself on a per-task _current_cancel_scope linked-list stack. In Task.__step, after the existing _must_cancel check, a new elif checks whether the current scope is cancelled and not shielded — if so, it re-injects CancelledError. The fast path (_current_cancel_scope is None) is a single pointer check with zero overhead.

Test plan

  • 51 new tests in test_asyncio.test_cancelscope covering both _PyTask and _CTask
  • Level-triggered re-injection (5 successive catches in a loop)
  • Deadline/timeout, cancel_scope()/cancel_scope_at() convenience APIs
  • shield=True blocking re-injection
  • Nested scopes (inner cancelled, outer cancelled, both cancelled)
  • cancelled_caught semantics
  • TaskGroup interaction
  • Edge-triggered task.cancel() unchanged
  • Full test_asyncio suite passes (2,646 tests, 0 failures)

🤖 Generated with Claude Code

Introduce CancelScope, an async context manager that provides
level-triggered cancellation semantics for asyncio. Once cancelled
(via cancel() or deadline expiry), every subsequent await inside
the scope raises CancelledError until the scope exits — the
coroutine cannot simply catch-and-ignore.

This integrates with Task.__step by checking the current scope's
state after the existing edge-triggered _must_cancel check. The
scope pushes/pops itself on a per-task _current_cancel_scope
linked-list stack. Existing edge-triggered mechanisms (cancel(),
uncancel(), Timeout, TaskGroup) remain completely unchanged.

New public API:
- asyncio.CancelScope(*, deadline=None, shield=False)
- asyncio.cancel_scope(delay, *, shield=False)
- asyncio.cancel_scope_at(when, *, shield=False)

Both the Python (_PyTask) and C (_CTask) Task implementations
are updated. Code not using CancelScope has zero overhead
(_current_cancel_scope is None → single pointer check).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ZeroIntensity
Copy link
Member

See my comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants