Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 15 – Async programming with asyncio

TaskGroup and structured concurrency

Run related asyncio tasks in a TaskGroup so that none outlives its block: collect results, see one failure cancel the rest, and compare it with gather.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: A task group's block waits for every task it started · One failure cancels the rest and raises an ExceptionGroup · gather or TaskGroup: choose by what one failure should do · 3 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Run related tasks inside an asyncio.TaskGroup and read their results
  • Handle the failures a task group reports as an ExceptionGroup with except*
  • Explain how one failing task cancels the rest of its group
  • Choose between gather and a task group for a set of calls
  • Apply limits, retries and early exits to the tasks of a group

Before you start

Three price lookups of 0.2, 0.1 and 0.3 seconds finish in about 0.3 seconds in an asyncio.TaskGroup, and the block that runs them does not end until all three are done. A TaskGroup runs a set of related tasks as one unit: you open it with async with asyncio.TaskGroup() as tg: and start tasks with tg.create_task(). If one of them fails, the TaskGroup cancels the others, waits for them to stop and raises an ExceptionGroup, an exception group: An exception that carries several others, made with ExceptionGroup(message, exceptions). except* handles each kind of exception inside it, and a group can hold other groups. More in “Exception groups and except*” that holds the failures, which you handle with except*.

This is structured concurrency: Writing concurrent code so that every task belongs to a block that cannot end before the task does, and so no task outlives the code that started it. In asyncio, the async with block of a TaskGroup gives that guarantee.: no task outlives the block that started it, just as no local variable outlives its function. The indentation shows how long the concurrent work lasts, and the forgotten, half-finished tasks that create_task() and gather() can leave behind cannot happen to the tasks a group starts. TaskGroup arrived in Python 3.11, and the asyncio documentation describes it as giving stronger safety guarantees than gather.

A task group’s block waits for every task it started

Three shops answer a price lookup at different speeds:

Three price lookups in a TaskGroup Python · price_lookups.py
"""Three price lookups in a TaskGroup: the block ends only when every task in it has finished."""
import asyncio
import time

SHOPS = {"shop-a": (0.20, 499), "shop-b": (0.10, 479), "shop-c": (0.30, 489)}  # reply time in s, price in rupees


async def price(shop):
    seconds, rupees = SHOPS[shop]
    await asyncio.sleep(seconds)  # stands in for an HTTP request
    return rupees


async def main():
    start = time.perf_counter()
    async with asyncio.TaskGroup() as tg:
        tasks = {shop: tg.create_task(price(shop), name=shop) for shop in SHOPS}
        print("inside the block: tasks created, none finished:", not any(t.done() for t in tasks.values()))
    print("after the block, every task is done:", all(t.done() for t in tasks.values()))
    prices = {shop: task.result() for shop, task in tasks.items()}
    best = min(prices, key=prices.get)
    print(f"prices: {prices}; lowest at {best}")
    print(f"took about {time.perf_counter() - start:.1f} s, the time of the slowest shop")


asyncio.run(main())

Output

inside the block: tasks created, none finished: True
after the block, every task is done: True
prices: {'shop-a': 499, 'shop-b': 479, 'shop-c': 489}; lowest at shop-b
took about 0.3 s, the time of the slowest shop

This output changes from run to run: the last line measures time, which varies a little from run to run

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 price_lookups.py

tg.create_task() works like asyncio.create_task(), with one difference: the task belongs to the group. Inside the block the tasks were only created. Leaving the block waited for all three, so afterwards every task was done and task.result() gave its value straight away. Step through the run on a clock:

Interactive · step by step The block ends when the last shop answers Step through price_lookups.py on a clock. The block on the left is the async with of the group; the three tasks belong to it.
The block ends when the last shop answers, step 6 of 6. Showing async with TaskGroup (lowest: shop-b, 0.3 s), shop-a (done: 499), shop-b (done: 479), shop-c (done: 489). In focus: async with TaskGroup.tg.create_taskasync with TaskGrouplowest: shop-b, 0.3 sshop-adone: 499shop-bdone: 479shop-cdone: 489

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Three tasks created. Inside the block, tg.create_task() creates one lookup per shop. They belong to the group and have not run yet, so the body prints that none has finished.
  2. The body ends. The body has nothing more to do, but leaving the block means waiting for every task of the group. The tasks start while it waits.
  3. 0.1 s, shop-b. shop-b answers first, with 479 rupees. The block still waits for the other two.
  4. 0.2 s, shop-a. shop-a answers with 499 rupees.
  5. 0.3 s, shop-c. shop-c answers last, with 489 rupees. Every task of the group is done, so the block ends.
  6. After the block. Every task is done, so task.result() gives each price at once. The lowest is at shop-b, and the whole lookup took about 0.3 s, the time of the slowest shop.

The three waits overlapped, and the lookup took as long as the slowest shop. Keeping the tasks in a dictionary keyed by shop is the usual way to read the results; a TaskGroup has no list of results of its own. Press Edit, give shop-c 0.05 seconds instead of 0.30 and run it again: the block now ends with shop-a, after about 0.2 seconds.

Checkpoint

Check: A group's block waits for its tasks

  1. Question 1 of 2 How does price_lookups.py read the three prices once the block has ended?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: With task.result() of each task, kept in a dictionary keyed by shop

    tg.create_task() returns the task, and the program keeps the three in a dictionary keyed by shop. After the block every task is done, so {shop: task.result() for shop, task in tasks.items()} reads the prices without any await.

  2. Question 2 of 2 Inside async with asyncio.TaskGroup() as tg: you call tg.create_task(slow()), where slow() takes 3 seconds, and the body itself ends after 1 second. When does the program reach the line after the block?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: After about 3 seconds, when slow() has finished

    Leaving the block waits for every task created through the group. The task starts at the body's first pause and runs while the body works, so the block ends when the slower of the two, slow(), is done.

One failure cancels the rest and raises an ExceptionGroup

Now shop-b does not list the product, and its lookup raises:

shop-b fails, shop-c is cancelled Python · one_fails.py
"""One task fails: the group cancels the others and raises an ExceptionGroup, handled here with except*."""
import asyncio

log = []


async def price(shop, seconds, fail=False):
    try:
        await asyncio.sleep(seconds)
        if fail:
            raise LookupError(f"{shop}: product not listed")
        log.append(f"{shop} answered")
        return 499
    except asyncio.CancelledError:
        log.append(f"{shop} cancelled")
        raise  # a cancelled task must let the cancellation through


async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(price("shop-a", 0.05))
            tg.create_task(price("shop-b", 0.10, fail=True))
            tg.create_task(price("shop-c", 0.30))
        print("this line is skipped")
    except* LookupError as group:
        print("caught:", type(group).__name__, "-", group.message)
        print("inside it:", [str(e) for e in group.exceptions])
    print("what happened:", log)


asyncio.run(main())

Output

caught: ExceptionGroup - unhandled errors in a TaskGroup
inside it: ['shop-b: product not listed']
what happened: ['shop-a answered', 'shop-c cancelled']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 one_fails.py

shop-a answered first. When shop-b raised its LookupError, the group cancelled shop-c, which was still waiting, waited until it had stopped and then raised an ExceptionGroup with the message “unhandled errors in a TaskGroup”. Step through what the group did, one move at a time:

Interactive · step by step shop-b fails, so the group cancels shop-c Step through one_fails.py on a clock. Watch what the group does to the task that is still running when another one fails.
shop-b fails, so the group cancels shop-c, step 7 of 7. Showing the TaskGroup (handled by except*), shop-a (answered), shop-b (raised LookupError), shop-c (cancelled at its await).the TaskGrouphandled by except*shop-aansweredshop-braised LookupErrorshop-ccancelled at its await

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Three lookups start. The group runs three price lookups. shop-b's will raise LookupError, because the product is not listed there.
  2. 0.05 s, shop-a answers. shop-a answers before anything goes wrong, and the log records shop-a answered.
  3. 0.1 s, shop-b raises. shop-b raises LookupError. The exception does not leave the task directly; the group receives it.
  4. The group cancels shop-c. The first failure makes the group cancel every task that is not done. shop-c is still waiting, so it gets CancelledError at its await.
  5. shop-c lets it through. shop-c records shop-c cancelled and raises the CancelledError again, as a cancelled task must. The group waits until it has stopped.
  6. An ExceptionGroup comes out. With every task stopped, the group raises ExceptionGroup("unhandled errors in a TaskGroup") holding the LookupError. It leaves the block, so the line after the block never runs.
  7. except* matches inside it. except* LookupError matches the error inside the group and prints it. A plain except LookupError would not match, because the group itself is not a LookupError.

The exception left the async with block, so the line after the block never ran. except* LookupError matched the error inside the group; a plain except LookupError would not have matched, because the group is not a LookupError, as the lesson on exception groups showed. Every task that raised before the cancellation took effect is in the group, so two shops that failed together would both be reported.

Checkpoint

Check: One failure cancels the rest

  1. Question 1 of 2 In one_fails.py, why would a plain except LookupError not catch shop-b's error?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The group raises an ExceptionGroup, which is not a LookupError; except* matches the error inside it

    A TaskGroup reports failures as an ExceptionGroup with the message "unhandled errors in a TaskGroup". The group is not a LookupError, so a plain except LookupError skips it, while except* LookupError matches the LookupError inside, as the lesson on exception groups showed.

  2. Question 2 of 2 Two lookups of a group raise at the same moment, before the group's cancellation can take effect. What does the ExceptionGroup hold?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Both errors

    A TaskGroup collects the exceptions of every task that failed, so two shops that failed together are both in group.exceptions, and except* clauses can handle each kind.

gather or TaskGroup: choose by what one failure should do

The two differ most when a call fails, which the first rows of the table set side by side:

gather() or a task group for calls that belong together?
Criterion gather()TaskGroup
One call fails the first error is raised; the other calls keep runningthe other tasks are cancelled, then an exception group is raised
Two fail at once only the first is raisedboth are in the group
Results a list in argument ordertask.result() of each task
Afterwards calls may still be runningnone: the block waits for all of them
New work no: the calls are fixedyes: tasks can add tasks to the group
Every outcome return_exceptions=Trueeach task catches its own expected errors
When to choose Independent calls where you want every outcomeCalls that must all succeed, and any new code

Use a TaskGroup for work that only makes sense as a whole, such as the parts of one page or the steps of one order. Use gather with return_exceptions=True when each call stands on its own and you want every outcome, good or bad. The table’s last row joins the two: a task that catches its own expected errors turns a group into a batch with partial results.

Start from a task group

In new code, start from a TaskGroup and step down to gather only when you want every outcome: the group’s way of failing is the safer default.

Checkpoint

Check: Choosing gather or a task group

  1. One question A product page needs price, stock and ratings, and shows an error page if any of the three fails. Which fits best?

    Choose one answer.

    Show the answer: One question

    Answer: A TaskGroup, so one failure cancels the other two and the error is raised once they have stopped

    Work that only makes sense as a whole belongs in a task group: one failure cancels the rest, and no call outlives the request. gather with return_exceptions=True suits calls that each stand on their own.

The one rule: every task goes through the group

The guarantee covers only the tasks that the group created. One asyncio.create_task() in the block breaks it:

A task made outside the group Python · stray_task.py
"""The one rule: every task of a group is made with tg.create_task(). A task made with asyncio.create_task() inside
the block is not part of the group, so the block neither waits for it nor cancels it."""
import asyncio

log = []


async def job(name, seconds):
    await asyncio.sleep(seconds)
    log.append(f"{name} finished")


async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(job("owned", 0.05))
        stray = asyncio.create_task(job("stray", 0.20))  # the mistake: not tg.create_task()
    print("block exited:", log, "| stray still running:", not stray.done())
    await stray  # without this line, nothing would ever wait for it
    print("much later:", log)


asyncio.run(main())

Output

block exited: ['owned finished'] | stray still running: True
much later: ['owned finished', 'stray finished']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stray_task.py

The block ended after 0.05 seconds, when the owned task was done, while the stray task was still running. Nothing in the group waited for it, and nothing would have cancelled it if another task had failed. Step through the two tasks:

Interactive · step by step The owned task and the stray one Step through stray_task.py. The solid arrow is the group's hold on its own task; the stray task has no arrow from the group.
The owned task and the stray one, step 4 of 4. Showing the TaskGroup (block has ended), owned (finished), stray (finished), main() (printed much later).waits for itthe TaskGroupblock has endedownedfinishedstrayfinishedmain()printed much laterfinished

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Two tasks, one group. Inside the block, owned is made with tg.create_task(), so the group knows it. stray is made with asyncio.create_task(), so the group knows nothing about it.
  2. 0.05 s, the block ends. owned finishes at 0.05 s. It is the only task the group waits for, so the block ends while stray is still running.
  3. stray is on its own. main() prints block exited with only owned finished, and stray still running True. Had another task failed, the group would not have cancelled stray either.
  4. 0.2 s, stray ends. stray finishes only because main() awaits it after the block. Without that line nothing would ever wait for it, or see its errors.

asyncio.create_task() inside a task group's block

asyncio.create_task(audit(order)) inside the block makes a task the group knows nothing about: the block may end while it runs, and a failure will not cancel it. Write tg.create_task(audit(order)), and pass tg to any coroutine that needs to start more work in the same group.

Once the block has ended, the group takes no new tasks: tg.create_task() then raises RuntimeError, and since Python 3.13 it also closes the coroutine it was given, so no “never awaited” warning follows the error.

Checkpoint

Check: Every task goes through the group

  1. Question 1 of 2 What does tg.create_task() do when it is called after the group's block has ended?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raises RuntimeError, and since Python 3.13 it also closes the coroutine it was given

    Once the block has ended, tg.create_task() raises RuntimeError. Python 3.13 added that it closes the coroutine it was given, so the error is not followed by a "coroutine was never awaited" warning.

  2. Question 2 of 2 Inside a task group's block, a developer writes asyncio.create_task(audit(order)) instead of tg.create_task(audit(order)). What changes?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The block neither waits for the audit task nor cancels it when another task fails

    A task group knows only the tasks made with its own create_task(). A task from asyncio.create_task() is an independent task: the block can end while it still runs, a failure in the group does not cancel it, and its errors reach nobody unless someone awaits it.

Five patterns that combine with a task group

Five patterns combine with a task group, each answering one question a real service asks:

Pattern What it adds to a group
A semaphore: A counter of permits, such as asyncio.Semaphore(20), that lets at most that many coroutines through at a time while the others wait for a permit, which caps how many calls run at the same moment. More in “Tasks, gather and running coroutines” Caps how many of the group’s tasks run their calls at the same moment
Partial results A task that catches its own expected errors gives an answer for its item without hiding real bugs
Retries with exponential backoff: Waiting longer before each new attempt, doubling the wait each time up to a cap, so that a failing service gets time to recover instead of a steady stream of retries. More in “Designing error handling for real programs” A flaky call tries again, waiting a little longer each time, before it fails the group
An early exit Stops the whole group once one task has the answer
asyncio.timeout() Puts one time limit around the whole group

Three of them live inside the tasks; the timeout and the early exit act on the whole block.

Checkpoint

Check: Patterns that combine with a group

  1. One question Which pattern stops the whole group as soon as one task has the answer?

    Choose one answer.

    Show the answer: One question

    Answer: An early exit

    Each pattern answers one question a service asks: a semaphore how many calls at once, partial results what one bad item means, retries how often to try again, an early exit when to stop, and a timeout how long to wait in all.

Check yourself

Practice on the five sections above follows, a question at a time, the idea with the fewest right answers first and a reason given for each choice.

Practice that adapts to you

Practise this lesson

10 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What does tg.create_task() give a task that asyncio.create_task() does not?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: The task belongs to the group, so the block waits for it and can cancel it

    tg.create_task() works like asyncio.create_task(), with one difference: the task belongs to the group, so the block does not end until it is done, and a failure elsewhere in the group cancels it.

  2. Question 2 of 10 Three lookups in a TaskGroup take 0.4 s, 0.15 s and 0.25 s. About how many seconds pass from entering the block to the line after it?

    Type a number.

    A hint

    The waits overlap, and the block waits for the last one.

    Show the answer: Question 2 of 10

    Answer: 0.4 seconds (anything from 0.35 to 0.45 counts)

    The three tasks wait at the same time, and the block ends when the slowest is done: about 0.4 s, not the 0.8 s the three add up to.

  3. Question 3 of 10 What does a TaskGroup raise when one of its tasks fails?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: An ExceptionGroup, "unhandled errors in a TaskGroup", that holds the failure

    On the first failure the group cancels the others, waits for them to stop and raises an ExceptionGroup with the message "unhandled errors in a TaskGroup" holding every failure.

  4. Question 4 of 10 What does one_fails.py print?

    What does this program print? Choose one answer.

    """One task fails: the group cancels the others and raises an ExceptionGroup, handled here with except*."""
    import asyncio
    
    log = []
    
    
    async def price(shop, seconds, fail=False):
        try:
            await asyncio.sleep(seconds)
            if fail:
                raise LookupError(f"{shop}: product not listed")
            log.append(f"{shop} answered")
            return 499
        except asyncio.CancelledError:
            log.append(f"{shop} cancelled")
            raise  # a cancelled task must let the cancellation through
    
    
    async def main():
        try:
            async with asyncio.TaskGroup() as tg:
                tg.create_task(price("shop-a", 0.05))
                tg.create_task(price("shop-b", 0.10, fail=True))
                tg.create_task(price("shop-c", 0.30))
            print("this line is skipped")
        except* LookupError as group:
            print("caught:", type(group).__name__, "-", group.message)
            print("inside it:", [str(e) for e in group.exceptions])
        print("what happened:", log)
    
    
    asyncio.run(main())
    Show the answer: Question 4 of 10

    Answer: it prints

    caught: ExceptionGroup - unhandled errors in a TaskGroup
    inside it: ['shop-b: product not listed']
    what happened: ['shop-a answered', 'shop-c cancelled']

    shop-a answered before anything went wrong. When shop-b raised, the group cancelled shop-c, which was still waiting, waited for it to stop and raised an ExceptionGroup that holds the LookupError. The exception left the async with block, so the line after it never ran, and except* LookupError received the group.

  5. Question 5 of 10 Which of the two cancels the calls that are still running when one call fails?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: A TaskGroup

    The table's first row: gather raises the first error and the other calls keep running; a TaskGroup cancels the other tasks, then raises an exception group.

  6. Question 6 of 10 A monitoring job checks 40 independent hosts and must report the status of every host, including the ones that fail. Which fits?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: gather(..., return_exceptions=True), then a report line per result

    Use gather with return_exceptions=True when each call stands on its own and you want every outcome, and a task group for work that only makes sense as a whole.

  7. Question 7 of 10 In stray_task.py, the block ends at 0.05 s. What is the state of the stray task at that moment?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Still running; nothing in the group waits for it

    owned, the group's task, finished at 0.05 s, and the block ended. stray, made with asyncio.create_task(), was not part of the group, so it was still running, and only the program's own await stray waited for it.

  8. Question 8 of 10 A coroutine running in a group needs to start more work that belongs to the same group. What does it need?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: The group itself, passed in as tg, so it can call tg.create_task()

    Inside a task group, create every task with tg.create_task(), and pass tg to a coroutine that needs to start more work in the same group.

  9. Question 9 of 10 An image service accepts at most 5 calls at once, and a group creates 50 thumbnail tasks. Which pattern keeps the group within the limit?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: A semaphore that caps how many tasks call the service at the same moment

    A semaphore caps how many of the group's tasks run their calls at the same moment, so 50 tasks can exist while only 5 talk to the service.

  10. Question 10 of 10 Which tool puts one time limit around a whole task group?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: asyncio.timeout()

    asyncio.timeout() around the group sets one limit on the whole of the work, the last of the patterns named in this section.

Failures and cancellation in depth, with two bugs reproduced Pro: opens with a Pro, Premium or Ultimate pass

A failing task sets off a fixed sequence inside its group, and two common bugs hide that sequence from the people who debug it. Below, the sequence is traced with timestamps, a whole group gets one time limit, and both bugs are reproduced.

In this part:

  • The cancellation sequence, traced with timestamps
  • A time limit around the whole group
  • Bug 1 reproduced: a one-line log that hides every error
  • Bug 2 reproduced: a stray task that writes after the request ended

1 diagram · 4 runnable examples (Python) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Patterns for real services: limits, partial results, retries and early exits Pro: opens with a Pro, Premium or Ultimate pass

Real services need more from a task group than running calls together: a cap on calls in flight, the results of the items that worked, retries for flaky calls and a way to stop as soon as the answer is known. Each pattern below runs inside a task group, and the exercise at the end combines two of them.

In this part:

  • Capping a group at 5 calls at a time, with the estimate
  • Partial results without hiding bugs
  • Retries with exponential backoff and jitter, with the call counts
  • Stopping early once the answer is known
  • Exercise: the first source that answers wins
  • The first source that answers wins, the rest are cancelled
    • Results of the sample tests

4 runnable examples (Python) · 1 exercise with sample tests · about 10 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Mid to senior

    Your service uses gather() for three calls. When one fails, the request returns an error, but the other two calls keep running. What changes if you switch to an asyncio.TaskGroup, and what can still go wrong?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A nightly job fetches 10,000 pages inside one TaskGroup. After 20 minutes, one page triggers a parsing bug, and the whole night's work is lost. Why, and how would you structure the job instead?

    Model answer, Pro. First step and follow-ups

  3. Fresher to senior

    Your logs show only "unhandled errors in a TaskGroup (3 sub-exceptions)" for failed checkouts. How do you see what actually failed, and what do you change so that the next failure is easy to diagnose?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • async with asyncio.TaskGroup() as tg: runs every task made with tg.create_task(), and the block waits for all of them.
  • When one task fails, the group cancels the others, waits for them and raises an ExceptionGroup with every failure; handle it with except*.
  • Structured concurrency means no task outlives the block that started it.
  • Prefer a TaskGroup for calls that must all succeed, and gather with return_exceptions=True for independent calls whose every outcome you want.
  • Create every task of the block through the group: a task from asyncio.create_task() escapes it.

Terms in this lesson

structured concurrency
Writing concurrent code so that every task belongs to a block that cannot end before the task does, and so no task outlives the code that started it. In asyncio, the async with block of a TaskGroup gives that guarantee.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.