GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-108951: add TaskGroup.cancel() by belm0 · Pull Request #127214 · python/cpython

Conversation

@belm0

@belm0 belm0 commented Nov 24, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Short-circuiting of task groups is a very common, useful, and normal, so make it a first-class operation. The recommended approach to date-- creating a task just to raise an exception, and then catch and suppress the exception-- is inefficient, prone to races, and requires a lot of boilerplate.

  • Issue: Add an asyncio.TaskGroup.cancel method #108951

📚 Documentation preview 📚: https://cpython-previews--127214.org.readthedocs.build/

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

MicaelJarniac reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@belm0
add TaskGroup.stop()
8ec2f60
ISSUE: python#108951
@belm0
belm0 requested review from 1st1, asvetlov, kumaraditya303 and willingc as code owners November 24, 2024 07:08
@bedevere-app bedevere-app Bot added the awaiting review label Nov 24, 2024
@bedevere-app bedevere-app Bot mentioned this pull request Nov 24, 2024
Add an asyncio.TaskGroup.cancel method #108951
Closed
blurb-it Bot and others added 2 commits November 24, 2024 07:18
@blurb-it
📜🤖 Added by blurb_it.
907f1d0
@belm0
minor doc fixes
2cfa1e6
sobolevn
sobolevn reviewed Nov 24, 2024
View reviewed changes

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This is not a full review, just a couple of questions.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Doc/library/asyncio-task.rst Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Doc/library/asyncio-task.rst Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Lib/test/test_asyncio/test_taskgroups.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Lib/test/test_asyncio/test_taskgroups.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Lib/test/test_asyncio/test_taskgroups.py Outdated

async def test_taskgroup_stop_children(self):
async with asyncio.TaskGroup() as tg:
tg.create_task(asyncio.sleep(math.inf))

@sobolevn sobolevn Nov 24, 2024

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe these tasks should look like this?

async def task(results, num):
    results.append(num)
    await asyncio.sleep(math.inf)
    results.append(-num)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@sobolevn sobolevn Nov 24, 2024

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can assert what was in results

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@belm0 belm0 Nov 24, 2024

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this particular test, I chose a different test approach, which is to wrap in asyncio.timeout().

For the other tests using count, I'm not sure it's much value, since the test code is only a few lines and there is only one possible path through it. So count result of 0, 1, or 2 each have deterministic meaning that's obvious by looking at the code.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Lib/test/test_asyncio/test_taskgroups.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@ZeroIntensity
ZeroIntensity requested a review from graingert November 24, 2024 16:13
@ZeroIntensity ZeroIntensity added the topic-asyncio label Nov 24, 2024
belm0 and others added 2 commits November 24, 2024 10:58
@belm0
make tests more explicit
bce1fb1
@belm0 @sobolevn
use versionadded:: next
7754aad
Co-authored-by: sobolevn <[email protected]>
1st1
1st1 requested changes Nov 25, 2024
View reviewed changes

@1st1 1st1 left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why call it TaskGroup.stop() and not TaskGroup.cancel()? I'd be more in favor of the latter name.

Short-circuiting of task groups is a very common, useful, and normal, so make it a first-class operation.

Any evidence of this statement? I'd like you to write up technical motivation + examples. That will be useful for the docs.

And speaking of the documentation, you should also show some recipies of how this would be used. Like are you supposed to use this API from within the task group async with clause? Or can you pass the task group to some remote task?

I haven't reviewed the actual implementation in detail yet.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@bedevere-app bedevere-app Bot added awaiting changes and removed awaiting review labels Nov 25, 2024
@bedevere-app

bedevere-app Bot commented Nov 25, 2024

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@arthur-tacca

arthur-tacca commented Nov 26, 2024

Copy link
Copy Markdown

This doesn't work in the case that the body of the task group throws an exception, as in this code:

    async def test_taskgroup_throw_inside(self):

        class MyError(RuntimeError):
            pass

        should_get_here = False
        try:
            async with asyncio.TaskGroup() as tg:
                tg.create_task(asyncio.sleep(0.1))
                tg.stop()
                self.assertEqual(asyncio.current_task().cancelling(), 1)
                raise MyError
            self.fail()  # <-- reaches here instead of raising ExceptionGroup([MyError()])
        except* MyError:
            self.assertEqual(asyncio.current_task().cancelling(), 0)
            should_get_here = True
        self.assertTrue(should_get_here)

The problem is that the new code in the _aexit() method, if not self._errors: return True, is essentially duplicating the if self._errors test later in the function, but in between self._errors is changed by these two lines:

        if et is not None and not issubclass(et, exceptions.CancelledError):
            self._errors.append(exc)

One option is move these lines earlier, before the if self._parent_cancel_requested statement. Then both tests are checking the same thing. This seems to work.

I'd still suggest my original proposal (see the issue) where you just add a single line return True to the very end of _exit() instead of these changes. This avoids duplicating the test in the first place and avoids changing the control flow and, personally, I find it easier to follow.

As a separate point, I'd suggest that the tests could do with a few more checks that asyncio.current_task().cancelling() is correct, like the ones in the test above in this comment.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@belm0

belm0 commented Nov 26, 2024

Copy link
Copy Markdown
Contributor Author

@1st1

Why call it TaskGroup.stop() and not TaskGroup.cancel()? I'd be more in favor of the latter name.

I'd also prefer cancel(), but per Guido it would be confusing since such a method would be expected to raise CancelledError, and he suggested stop().

Short-circuiting of task groups is a very common, useful, and normal, so make it a first-class operation.

Any evidence of this statement? I'd like you to write up technical motivation + examples. That will be useful for the docs.

In trio the equivalent is nursery.cancel_scope.cancel(), which has > 1,000 hits on github, despite the unpopularity of trio.

I have years experience developing a non-trivial, production async app, which I've presented at PyCon JP. Anecdotally, I can't imagine how painful and unproductive it would be to not have short circuiting of task groups.

And speaking of the documentation, you should also show some recipies of how this would be used. Like are you supposed to use this API from within the task group async with clause? Or can you pass the task group to some remote task?

All is on the table: stop from within the TaskGroup body, from a child, from some other entity you've passed the bound stop() method to.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@m-u-xyz

m-u-xyz commented Nov 26, 2024

Copy link
Copy Markdown

I'd also prefer cancel(), but per Guido it would be confusing since such a method would be expected to raise CancelledError,

Well, that's exactly what it does, isn't it? The fact that the cancelled taskgroup catches the CancelledErrors raised by itself doesn't change that. You don't get to wait on taskgroups the way you wait on tasks, thus the exception isn't visible like when you await on a cancelled task, but that's a minor detail IMHO.

Also, trio and anyio already call this operation cancel.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

belm0 added 2 commits November 29, 2024 16:38
@belm0
stop() -> cancel()
452042d
@belm0
document some ways to use cancel()
f077241
@belm0
belm0 force-pushed the task_group_stop branch from 7e25c26 to f077241 Compare November 30, 2024 00:38
graingert
graingert reviewed Nov 30, 2024
View reviewed changes
Comment thread Doc/library/asyncio-task.rst

Ways to use :meth:`cancel`:

* call it from the task group body based on some condition or event

@graingert graingert Nov 30, 2024

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably you want code examples for all of these?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

gvanrossum reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@belm0 belm0 changed the title gh-108951: add TaskGroup.stop() gh-108951: add TaskGroup.cancel() Nov 30, 2024
belm0 added 2 commits November 30, 2024 16:12
@belm0
fix cases of exception in task group body before/after cancel()
8345647
@belm0
add test for create_task() following cancel()
7235c20
graingert
graingert reviewed Dec 15, 2024
View reviewed changes
Comment thread Lib/asyncio/taskgroups.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

graingert
graingert reviewed Dec 15, 2024
View reviewed changes
Comment thread Lib/test/test_asyncio/test_taskgroups.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@belm0
belm0 requested review from StanFromIreland and hugovk as code owners April 23, 2026 02:56
belm0 added 2 commits April 22, 2026 20:47
@belm0
move test methods back to BaseTestTaskGroup (caused by bad merge)
e8617b4
@belm0
add race test
8710b95
@hugovk
hugovk removed their request for review April 23, 2026 05:10
@gvanrossum

gvanrossum commented Apr 23, 2026

Copy link
Copy Markdown
Member

@belm0 -- I take it you are not planning to add cancel messages and you are not planning to add more examples to the docs? Once you confirm that I will approve and merge.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@belm0
small doc refinements
2cd33e3
belm0
belm0 commented Apr 24, 2026
View reviewed changes

@belm0 belm0 left a comment

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it you are not planning to add cancel messages and you are not planning to add more examples to the docs?

I've been working through the PR's items in some priority order and hadn't reached those (you've caught me in the middle of quite a hectic week).

I've just committed a few tweaks to the added docs, but no cancel examples. Task groups are quite powerful and I've seen other async libs build up examples over several pages. The existing docs are lacking here so I don't have the foundation to add the cancel() examples. Expanded TaskGroup docs need to be written by someone who lives and breathes asyncio daily and is intimate with the library's nuances (that was once me for trio, but not asyncio).

For cancel(msg) I've made another comment on that thread-- not being bought into the idea I'm reluctant to take on the extra impl/doc/testing in this PR.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Lib/asyncio/taskgroups.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@belm0
fix doc lint
edcd97e
gvanrossum
gvanrossum reviewed Apr 24, 2026
View reviewed changes

@gvanrossum gvanrossum left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; I'll merge it.

I do have some nits on the docs still but they don't need to hold up the merge.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

arthur-tacca reacted with hooray emoji
All reactions
  • 🎉 1 reaction
gvanrossum
gvanrossum approved these changes Apr 24, 2026
View reviewed changes
@bedevere-app bedevere-app Bot added the awaiting merge label Apr 24, 2026
@gvanrossum
gvanrossum merged commit 95559d2 into python:main Apr 24, 2026
53 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot removed the awaiting merge label Apr 24, 2026
@gvanrossum

gvanrossum commented Apr 24, 2026

Copy link
Copy Markdown
Member

@belm0 Thanks for your perseverance! This looks like a useful addition.

Additionally we probably need a "what's new" entry, and I have some nits on the docs (mostly that I find the term "short-circuit" confusing, I've not seen it in the context of asyncio or task groups).

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@agronholm

agronholm commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

I was unaware of this effort. Good to see this being added.

I have some nits on the docs, though. It says:

cancel() will be called on any tasks in the group that aren't yet
done, as well as the parent (body) of the group. This will cause the
task group context manager to exit without asyncio.CancelledError
being raised.

As opposed to what? CancelledError being raised from the taskgroup? Does cancel() have any effect on that?

And the news item:

Add :meth:~asyncio.TaskGroup.cancel which cancels unfinished tasks and exits the group without error.

Surely it will exit with errors if the tasks raise any non-cancellation exceptions after TaskGroup.cancel()?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@m-u-xyz

m-u-xyz commented Apr 25, 2026

Copy link
Copy Markdown

As opposed to what?

I think that's supposed to be "as opposed to cancelling the task in which the taskgroup is running".

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@agronholm

agronholm commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

As opposed to what?

I think that's supposed to be "as opposed to cancelling the task in which the taskgroup is running".

But it does cancel the host task too? It just uncancels it when exiting the task group.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@m-u-xyz

m-u-xyz commented Apr 25, 2026

Copy link
Copy Markdown

But it does cancel the host task too?

That would be … quite non-productive.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@agronholm

agronholm commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

AnyIO task groups and Trio nurseries do that too. Do you find that unproductive?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@gvanrossum

gvanrossum commented Apr 25, 2026

Copy link
Copy Markdown
Member

@agronholm Are you still talking about the docs or are you now disagreeing with how it works?

If the former, feel free to send a PR.

If the latter, I suggest reading the full discussion in the issue first, and then everything here. Use an AI to summary if you want to.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@agronholm

agronholm commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

@agronholm Are you still talking about the docs or are you now disagreeing with how it works?

If the former, feel free to send a PR.

If the latter, I suggest reading the full discussion in the issue first, and then everything here. Use an AI to summary if you want to.

I'm not trying to argue about the functionality, I only have some slight problems about the wordings and how they could be misleading.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@m-u-xyz

m-u-xyz commented Apr 25, 2026

Copy link
Copy Markdown

@agronholm Misunderstanding here. "Cancel the host task", in asyncio parlance, meant cancelling the task directly, not the taskgroup. in that case the taskgroup won't eat the cancellation exception.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@agronholm

agronholm commented Apr 25, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

@agronholm Misunderstanding here. "Cancel the host task", in asyncio parlance, meant cancelling the task directly, not the taskgroup. in that case the taskgroup won't eat the cancellation exception.

There is no way to accomplish this on asyncio without calling Task.cancel(). And yes, the task group will eat the cancellation exception (via uncancellation) unless .cancel() was also called from elsewhere.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

ljfp pushed a commit to ljfp/cpython that referenced this pull request Apr 25, 2026
@belm0 @sobolevn
@asvetlov @gvanrossum @ljfp
pythongh-108951: add TaskGroup.cancel() (python#127214)
5a4a1a7
Fixes python#108951

Co-authored-by: sobolevn <[email protected]>
Co-authored-by: Andrew Svetlov <[email protected]>
Co-authored-by: Guido van Rossum <[email protected]>
@belm0
belm0 deleted the task_group_stop branch April 26, 2026 22:19
@belm0 belm0 mentioned this pull request Apr 26, 2026
gh-108951: document addition of TaskGroup.cancel() #149031
Merged
@belm0

belm0 commented Apr 26, 2026

Copy link
Copy Markdown
Contributor Author

whatsnew entry and minimal doc tweaks in #149031

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

This was referenced May 8, 2026
Python 3.15: sort allowlists, more small updates python/typeshed#15714
Merged
Add small Python 3.15 stdlib updates python/typeshed#15727
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@graingert graingert graingert left review comments
@sobolevn sobolevn sobolevn left review comments
@gvanrossum gvanrossum gvanrossum approved these changes
@asvetlov asvetlov Awaiting requested review from asvetlov asvetlov is a code owner
@kumaraditya303 kumaraditya303 Awaiting requested review from kumaraditya303 kumaraditya303 is a code owner
@willingc willingc Awaiting requested review from willingc willingc is a code owner
@1st1 1st1 Awaiting requested review from 1st1 1st1 is a code owner
@AA-Turner AA-Turner Awaiting requested review from AA-Turner
@StanFromIreland StanFromIreland Awaiting requested review from StanFromIreland
+1 more reviewer
@m-u-xyz m-u-xyz m-u-xyz left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

stale Stale PR or inactive for long period of time. topic-asyncio

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

10 participants

@belm0 @arthur-tacca @m-u-xyz @graingert @gvanrossum @agronholm @1st1 @asvetlov @sobolevn @ZeroIntensity
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.