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

Async iterators, generators and context managers

Consume async iterables with async for, write async generators and async context managers, and close them on time with aclosing and AsyncExitStack.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: An async generator fetches a page only when the loop asks · async for and async with call the awaited twins of for and with · asynccontextmanager runs the code after yield on every way out · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Read async iterables with async for, aiter() and anext()
  • Write async generators that fetch data page by page
  • Write async context managers with asynccontextmanager and with classes
  • Prevent leaks by closing async generators with contextlib.aclosing
  • Manage a varying number of async resources with AsyncExitStack

Before you start

An API that sends orders two to a page needs a network round trip for every page, and a plain for loop cannot wait for one without blocking the event loop. async iterator: An object that async for can loop over: __aiter__() returns it, and each await of its __anext__() gives the next item or raises StopAsyncIteration at the end. Unlike a plain iterator, it may wait between items. can: they are what async for loops over, objects that hand out their items one at a time and may await between them. The rows of a database query and the pages of an API are typical. The easy way to write one is an async generator: A function defined with async def whose body contains yield, and the async iterator that calling it returns. Its body runs only while the next item is awaited, so it can await a page of data and then yield the page's items one by one., an async def function that contains yield, so it can await the next page and then yield its items.

async context manager: An object with the coroutine methods __aenter__() and __aexit__(). async with awaits the first before its block and the second after it, however the block ends; contextlib.asynccontextmanager makes one from an async generator. are the matching tool for async with: their __aenter__() and __aexit__() are coroutines, so opening a connection or committing a transaction can wait without blocking the loop, and contextlib.asynccontextmanager builds one from an async generator. Both protocols mirror the synchronous for and with exactly, with one difference: every step may await. Each one is stepped through below on a real run.

An async generator fetches a page only when the loop asks

Each fetch here waits 0.01 seconds in place of an HTTP request:

Orders, one page at a time Python · paginated_api.py
"""An async generator that pages through an API: each page is fetched only when the loop asks for more."""
import asyncio

# A fake API with three pages of orders; the fourth page is empty, which means "no more".
PAGES = {1: ["A-101", "A-102"], 2: ["A-103", "A-104"], 3: ["A-105"], 4: []}


async def fetch_page(number):
    await asyncio.sleep(0.01)  # stands in for an HTTP request
    print(f"  (fetched page {number})")
    return PAGES[number]


async def orders():
    """Yield every order, one page at a time, until an empty page."""
    page = 1
    while True:
        batch = await fetch_page(page)
        if not batch:
            return
        for order in batch:
            yield order
        page += 1


async def main():
    async for order in orders():
        print("processing", order)
    first_three = [order async for order in orders() if order != "A-102"][:3]
    print("an async comprehension:", first_three)


asyncio.run(main())

Output

  (fetched page 1)
processing A-101
processing A-102
  (fetched page 2)
processing A-103
processing A-104
  (fetched page 3)
processing A-105
  (fetched page 4)
  (fetched page 1)
  (fetched page 2)
  (fetched page 3)
  (fetched page 4)
an async comprehension: ['A-101', 'A-103', 'A-104']

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

orders() fetched page 1, yielded its two orders and paused. Only when the loop asked for a third order did it fetch page 2, so the fetching and the processing took turns, and an empty page ended the generator with return. Step through the first loop:

Interactive · message by message The loop asks, the generator fetches Step through the first loop of paginated_api.py. Each arrow is one call or one value; watch when the generator fetches a page.
The loop asks, the generator fetches, step 9 of 9. Messages so far: async for to orders(): __anext__(); orders() to fetch_page(): fetch_page(1); fetch_page() to orders(): ['A-101', 'A-102']; orders() to async for: A-101; orders() to async for: A-102; orders() to fetch_page(): fetch_page(2); orders() to async for: A-103, A-104, A-105; fetch_page() to orders(): []; orders() to async for: StopAsyncIteration.async forthe loop in main()orders()finishedfetch_page()the fake API__anext__()fetch_page(1)['A-101', 'A-102']A-101A-102fetch_page(2)A-103, A-104, A-105[]StopAsyncIteration

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

Every step as text
  1. The first request. async for calls __aiter__() once and awaits __anext__() for the first order. Only now does the body of orders() start to run.
  2. Page 1 is fetched. The body awaits fetch_page(1), a network round trip. While it waits, the event loop is free to run other tasks.
  3. Two orders arrive. Page 1 holds two orders, and the program prints (fetched page 1).
  4. yield A-101. The generator yields A-101 and pauses at its yield. The loop prints processing A-101.
  5. No fetch for A-102. The loop asks again. The generator resumes after its yield and hands out A-102 from the page it already holds, without a round trip.
  6. Page 2, only now. The third request finds page 1 used up, so only now does the body fetch page 2. Fetching and processing take turns.
  7. The same for pages 2 and 3. Page 2 gives A-103 and A-104, then page 3 gives A-105, each page fetched when the loop has used up the one before.
  8. An empty page. fetch_page(4) returns an empty list, so the body runs return and the generator is finished.
  9. The loop ends. A finished body makes __anext__() raise StopAsyncIteration, which async for catches. The loop ends quietly, after four fetches and five orders.

Async generators are lazy in the same way as the generators of an earlier lesson: nothing runs until something asks for an item, and a page is held in memory only while its items are being handed out. Each yield is also where the consumer’s own work runs, so a slow consumer slows the producer down: an async generator never runs ahead of the loop that reads it.

The async comprehension at the end shows the opposite. [order async for order in orders() if ...] builds the whole list, so it fetched all four pages before [:3] kept three orders. Comprehensions, async ones included, collect everything; to stop early, use an async for loop with break.

Key idea

An async generator runs only while the loop awaits its next item, so it never runs ahead of the code that reads it.

Checkpoint

Check: An async generator fetches lazily

  1. Question 1 of 2 In paginated_api.py, when does orders() fetch page 2?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: When the loop asks for a third order, after A-101 and A-102 have been processed

    The output shows (fetched page 2) after processing A-102. The generator hands out the orders of the page it holds and fetches the next page only when the loop asks for an order that is not on it.

  2. Question 2 of 2 A loop over orders() leaves with break right after it has processed its second order. How many pages has it fetched?

    Type a number.

    A hint

    Which page holds the second order?

    Show the answer: Question 2 of 2

    Answer: 1 pages

    A-101 and A-102 are both on page 1, so the generator never needed page 2. A lazy generator fetches only what the loop has asked for: one page here, where the async comprehension fetched all four.

async for and async with call the awaited twins of for and with

As classes, Countdown and Timer show every method that async for and async with call:

An async iterator and an async context manager as classes Python · async_protocols.py
"""What async for and async with call: __aiter__ and __anext__, __aenter__ and __aexit__."""
import asyncio


class Countdown:
    """An async iterator written as a class."""

    def __init__(self, start):
        self.n = start

    def __aiter__(self):
        return self

    async def __anext__(self):
        if self.n == 0:
            raise StopAsyncIteration  # the async version of StopIteration
        await asyncio.sleep(0.01)
        self.n -= 1
        return self.n + 1


class Timer:
    """An async context manager written as a class."""

    async def __aenter__(self):
        print("  __aenter__: start")
        return self

    async def __aexit__(self, exc_type, exc, tb):
        print(f"  __aexit__: stop (exception: {exc_type.__name__ if exc_type else None})")
        return False  # do not suppress an exception


async def main():
    async for n in Countdown(3):
        print("async for got", n)
    it = aiter(Countdown(2))  # the built-ins aiter() and anext() do what the statements do
    print("anext:", await anext(it), await anext(it), await anext(it, "done"))
    async with Timer():
        print("  inside the block")
    try:
        async with Timer():
            raise ValueError("bad row")
    except ValueError as error:
        print("the error went on:", error)


asyncio.run(main())

Output

async for got 3
async for got 2
async for got 1
anext: 2 1 done
  __aenter__: start
  inside the block
  __aexit__: stop (exception: None)
  __aenter__: start
  __aexit__: stop (exception: ValueError)
the error went on: bad row

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

Step through the same run. async for calls __aiter__() once and then awaits __anext__() for every item until it raises StopAsyncIteration: The exception an async iterator's __anext__() raises when no items are left. async for catches it and ends quietly, as for does with StopIteration.; async with awaits __aenter__() before the block and __aexit__() after it:

Interactive · message by message What async for and async with call Step through async_protocols.py. Each arrow is a method call the statement makes, or the value it gets back.
What async for and async with call, step 11 of 11. Messages so far: main() to Countdown(3): __aiter__(); main() to Countdown(3): await __anext__(); Countdown(3) to main(): 3; Countdown(3) to main(): 2, then 1; Countdown(3) to main(): StopAsyncIteration; main() to Timer(): await __aenter__(); main() to Timer(): __aexit__(None, None, None); main() to Timer(): __aexit__(ValueError, …); Timer() to main(): False.main()caught: bad rowCountdown(3)n = 0Timer()an async context manager__aiter__()await __anext__()32, then 1StopAsyncIterationawait __aenter__()__aexit__(None, None, None)__aexit__(ValueError, …)False

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

Every step as text
  1. __aiter__() once. async for calls __aiter__() once. Countdown returns itself, the object that hands out the items.
  2. Await the first item. Then it awaits __anext__() for the first item. The method sleeps 0.01 s, a wait that a plain __next__() could not make, and lowers n to 2.
  3. The item: 3. __anext__() returns 3, and the body of the loop prints async for got 3.
  4. Two more items. The next two awaits return 2 and 1, and n reaches 0.
  5. StopAsyncIteration. The fourth __anext__() finds n at 0 and raises StopAsyncIteration, the async version of StopIteration. async for catches it, and the loop ends.
  6. aiter() and anext(). The built-ins aiter() and anext() call the same two methods by hand. On Countdown(2) the program gets 2 and 1, and anext(it, "done") returns its default instead of raising: "anext: 2 1 done".
  7. async with enters. async with Timer() awaits __aenter__(), which prints "__aenter__: start". What it returns is what as would receive.
  8. The block runs. The body of the block runs and prints inside the block.
  9. async with exits. At the end of the block it awaits __aexit__() with three None values, because no exception left the block, and Timer prints "(exception: None)".
  10. An error in the block. The second block raises ValueError. async with awaits __aexit__() with the exception's type, value and traceback, and Timer prints "(exception: ValueError)".
  11. The error goes on. __aexit__() returns False, so the exception is not suppressed. It leaves the block, and the except around it prints "the error went on: bad row".

Each has a synchronous twin:

The synchronous protocols and their async twins
Criterion for and withasync for and async with
Iterator methods __iter__() and __next__()__aiter__() and async __anext__()
End of the items StopIterationStopAsyncIteration
Built-in functions iter() and next()aiter() and anext(), since Python 3.10
Generator def with yieldasync def with yield
Context manager methods __enter__() and __exit__()async __aenter__() and __aexit__()
From a generator contextlib.contextmanagercontextlib.asynccontextmanager
Where it may be used anywhereinside async def only
When to choose Data that is already in memoryData or resources you have to wait for

Async iterators written as classes, like Countdown, are rare in application code, because an async generator does the same in a few lines; the class form matters when an object needs other methods too. anext(it, default) returns the default instead of raising at the end, as next() does. __aexit__() receives the exception that left the block, or three None values, and a true return value would suppress the exception, exactly as in the synchronous context managers of the errors module.

Swapping the two statements is a TypeError, and Python 3.14 names the right one:

with and async with, swapped Python · with_vs_async_with.py
"""Python 3.14 explains the mix-up between with and async with in the error message."""
import asyncio
import contextlib


@contextlib.asynccontextmanager
async def session():
    yield "session"


@contextlib.contextmanager
def lock_file():
    yield "lock"


async def main():
    try:
        with session():
            pass
    except TypeError as error:
        print("with on an async context manager:\n ", error)
    try:
        async with lock_file():
            pass
    except TypeError as error:
        print("async with on a plain context manager:\n ", error)
    try:
        async for _ in [1, 2]:
            pass
    except TypeError as error:
        print("async for on a list:\n ", error)


asyncio.run(main())

Output

with on an async context manager:
  'contextlib._AsyncGeneratorContextManager' object does not support the context manager protocol (missed __exit__ method) but it supports the asynchronous context manager protocol. Did you mean to use 'async with'?
async with on a plain context manager:
  'contextlib._GeneratorContextManager' object does not support the asynchronous context manager protocol (missed __aexit__ method) but it supports the context manager protocol. Did you mean to use 'with'?
async for on a list:
  'async for' requires an object with __aiter__ method, got list

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

Version note

The hints in these messages, “Did you mean to use ‘async with’?” and “Did you mean to use ‘with’?”, are new in Python 3.14. Python 3.13 says only that the object does not support the context manager protocol.

Checkpoint

Check: What async for and async with call

  1. Question 1 of 2 What makes an async for loop end?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: __anext__() raises StopAsyncIteration

    async for calls __aiter__() once and awaits __anext__() for every item. Countdown raised StopAsyncIteration when n reached 0, and the loop ended after printing 3, 2 and 1.

  2. Question 2 of 2 On Python 3.14, a plain with session(): is used on an object made by @contextlib.asynccontextmanager. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: TypeError, with the hint: Did you mean to use 'async with'?

    with_vs_async_with.py printed the 3.14 message: the object does not support the context manager protocol but supports the asynchronous one, "Did you mean to use 'async with'?".

asynccontextmanager runs the code after yield on every way out

This generator lends a connection from a small pool:

Borrow a connection, always give it back Python · pool_context.py
"""asynccontextmanager turns an async generator into an async context manager: here, borrowing a connection from a
small pool, and always giving it back."""
import asyncio
import contextlib

free = ["conn-1", "conn-2"]


@contextlib.asynccontextmanager
async def borrowed():
    await asyncio.sleep(0.01)  # stands in for waiting for a free connection
    conn = free.pop()
    print(f"  borrowed {conn}")
    try:
        yield conn  # the body of the async with block runs here
    finally:
        free.append(conn)
        print(f"  returned {conn}")


async def main():
    async with borrowed() as conn:
        print("  query on", conn)
    try:
        async with borrowed() as conn:
            raise ConnectionError(f"{conn} dropped the query")
    except ConnectionError as error:
        print("  caught:", error)
    print("free connections:", sorted(free))


asyncio.run(main())

Output

  borrowed conn-2
  query on conn-2
  returned conn-2
  borrowed conn-2
  returned conn-2
  caught: conn-2 dropped the query
free connections: ['conn-1', 'conn-2']

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

Everything before the yield is the __aenter__() part, and the value yielded is what as conn receives. The body of the async with block runs at the yield, and the finally after it is the __aexit__() part, which ran both after a normal block and after the ConnectionError. Because both parts are coroutines, waiting for an idle connection or for a network round trip to close one does not block the event loop. Step through both blocks:

Interactive · step by step The code after yield gives the connection back Step through pool_context.py. The block on the left uses the connection; the generator on the right lends it and takes it back.
The code after yield gives the connection back, step 8 of 8. Showing async with block (caught: conn-2 dropped …), borrowed() (returned conn-2), free connections (conn-1, conn-2).yield connpop and appendasync with blockcaught: conn-2 dropped …borrowed()returned conn-2free connectionsconn-1, conn-2

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

Every step as text
  1. Before the yield. Entering the first block runs borrowed() up to its yield. It waits 0.01 s for a connection and takes conn-2 from the free list. This part plays __aenter__().
  2. yield conn-2. The value of the yield is what as conn receives. The generator pauses at its yield while the body of the block runs.
  3. The body runs. The body prints query on conn-2 and reaches the end of the block without an error.
  4. After the yield. The generator resumes after its yield, and its finally puts conn-2 back on the free list. This part plays __aexit__().
  5. Borrowed again. The second block borrows conn-2 again and gets it at the yield.
  6. The body raises. This time the body raises ConnectionError, conn-2 dropped the query. The generator is still paused at its yield.
  7. Thrown in at the yield. asynccontextmanager throws the exception into the generator at its yield. The exception runs the finally block on its way out, so conn-2 goes back before anything else happens.
  8. The error goes on. The generator does not catch the error, so it leaves the block, and the except around the block prints it. Both connections are free again.

A yield without try and finally

conn = free.pop(), yield conn, free.append(conn) loses the connection whenever the block raises: the exception is raised inside the generator at the yield, so the line after it never runs. Put the clean-up in finally, as borrowed() does, and it runs after a normal block, after an exception and after a cancellation, which arrives at the yield as CancelledError.

Checkpoint

Check: asynccontextmanager and its finally

  1. Question 1 of 2 In pool_context.py, what does as conn receive?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The value the generator yields, the connection it took

    Everything before the yield plays __aenter__(), and the yielded value, conn-2 in the run, is what as conn receives while the body of the block runs.

  2. Question 2 of 2 A version of borrowed() has no try and finally: conn = free.pop(), yield conn, free.append(conn). The body of the block raises ConnectionError. What happens to the connection?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It never goes back, because the error is raised at the yield and free.append(conn) never runs

    asynccontextmanager throws the block's exception into the generator at its yield. Without finally, the clean-up line is skipped and the connection is lost, after every error and after every cancellation.

Three tools close generators and resources on time

A cursor, a socket or a pooled connection can be held too long in two ways. A loop that leaves with break stops asking for items, but its generator stays paused at a yield instead of closed; and a stream that stops sending leaves await anext() waiting with no end. Three tools from the standard library answer them:

Tool What it does
contextlib.aclosing(gen) Closes an async generator when its async with block ends, even if the loop inside stopped early
contextlib.AsyncExitStack Enters a number of async context managers known only at run time, and exits them all in reverse order
asyncio.timeout() around each anext() Gives each item of a stream its own time limit

Checkpoint

Check: Tools that close on time

  1. Question 1 of 2 A loop over a generator that holds a database cursor may leave with break, and the cursor must close as soon as the loop is left. Which tool fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: contextlib.aclosing() around the generator

    contextlib.aclosing(gen) closes an async generator when its async with block ends, which is exactly when the loop inside it is left, by break or otherwise.

  2. Question 2 of 2 Which tool gives each item of a stream its own time limit?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: asyncio.timeout() around each anext()

    Putting anext() under asyncio.timeout() limits the wait for one item at a time, which is the third tool of the table.

Check yourself

One question at a time on paging, the two protocols, asynccontextmanager and the closing tools, the weakest idea first.

Practice that adapts to you

Practise this lesson

10 questions about 9 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 makes a function an async generator?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: It is defined with async def and contains yield

    An async generator is an async def function whose body contains yield, such as orders(): it can await a page and then yield the page's items.

  2. Question 2 of 10 In paginated_api.py, the async comprehension keeps only three orders with [:3]. How many pages does it fetch?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: All four, because the comprehension builds the whole list before the slice runs

    [order async for order in orders() if ...] is a list comprehension: it runs the async generator to its end and only then does [:3] cut the list. To stop early, break out of an async for loop instead, and close the generator.

  3. Question 3 of 10 An async generator pages through an API, and the caller needs only the first order that matches a test. Which code fetches the fewest pages?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: An async for loop that returns or breaks at the first match

    Laziness pays only when the consumer stops asking. A loop that leaves at the first match fetches the pages up to it and no more; anything that builds a list fetches them all.

  4. Question 4 of 10 A class defines async def __aenter__(self) and async def __aexit__(self, exc_type, exc, tb). Which statement uses it?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: async with, inside a coroutine

    __aenter__() and __aexit__() form the asynchronous context manager protocol, used by async with, which is allowed only inside an async def. A plain with needs __enter__() and __exit__(); Python 3.14's error message says so when the two are mixed up.

  5. Question 5 of 10 What does async_protocols.py print?

    What does this program print? Choose one answer.

    """What async for and async with call: __aiter__ and __anext__, __aenter__ and __aexit__."""
    import asyncio
    
    
    class Countdown:
        """An async iterator written as a class."""
    
        def __init__(self, start):
            self.n = start
    
        def __aiter__(self):
            return self
    
        async def __anext__(self):
            if self.n == 0:
                raise StopAsyncIteration  # the async version of StopIteration
            await asyncio.sleep(0.01)
            self.n -= 1
            return self.n + 1
    
    
    class Timer:
        """An async context manager written as a class."""
    
        async def __aenter__(self):
            print("  __aenter__: start")
            return self
    
        async def __aexit__(self, exc_type, exc, tb):
            print(f"  __aexit__: stop (exception: {exc_type.__name__ if exc_type else None})")
            return False  # do not suppress an exception
    
    
    async def main():
        async for n in Countdown(3):
            print("async for got", n)
        it = aiter(Countdown(2))  # the built-ins aiter() and anext() do what the statements do
        print("anext:", await anext(it), await anext(it), await anext(it, "done"))
        async with Timer():
            print("  inside the block")
        try:
            async with Timer():
                raise ValueError("bad row")
        except ValueError as error:
            print("the error went on:", error)
    
    
    asyncio.run(main())
    Show the answer: Question 5 of 10

    Answer: it prints

    async for got 3
    async for got 2
    async for got 1
    anext: 2 1 done
      __aenter__: start
      inside the block
      __aexit__: stop (exception: None)
      __aenter__: start
      __aexit__: stop (exception: ValueError)
    the error went on: bad row

    Countdown(3) hands out 3, 2 and 1, then raises StopAsyncIteration, which ends the loop. The third anext() gets its default, "done", instead of the exception. __aexit__() receives the ValueError and returns False, so the error goes on and the except around the block prints it.

  6. Question 6 of 10 it = aiter(Countdown(1)). What do two calls of await anext(it, "done") return?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: 1, then "done"

    anext(it, default) awaits the next item and returns the default when the iterator raises StopAsyncIteration, the async twin of next(it, default).

  7. Question 7 of 10 In an @asynccontextmanager generator, which part plays __aexit__()?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: The code after the yield, best inside finally

    Everything before the yield plays __aenter__(), the yielded value goes to as, and the code after the yield, inside finally, plays __aexit__().

  8. Question 8 of 10 A task waiting inside async with borrowed() as conn: is cancelled. What does the try and finally around the yield do?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: The cancellation arrives at the yield as CancelledError, and finally gives the connection back before it goes on

    An exception in the block, or a cancellation, is raised inside the generator at the yield. With try and finally around the yield, the connection goes back on every way out, and the cancellation goes on.

  9. Question 9 of 10 Which tool enters a number of async context managers known only at run time and exits them all in reverse order?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: contextlib.AsyncExitStack

    AsyncExitStack enters any number of async context managers and exits them all in reverse order when its block ends.

  10. Question 10 of 10 A job opens one connection per shard, and the number of shards comes from a configuration file. Which tool keeps the code to one block that closes them all?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: contextlib.AsyncExitStack, entering one connection per shard

    An AsyncExitStack is the tool for a number of async context managers known only at run time: one block enters them all and exits them in reverse order.

Closing async generators on time: abandoned loops, stalled streams and aclosing Pro: opens with a Pro, Premium or Ultimate pass

An async generator that holds a resource, a cursor or a socket, keeps it until the generator is closed, and leaving a loop early does not close it. Below, that leak is reproduced, a stream that stops sending is caught, and the fixes are compared.

In this part:

  • An abandoned generator, reproduced: the cursor closes late or only at shutdown
  • A time limit per item: catching a stream that stops sending
  • Choosing the fix: aclosing, a time limit per item or a queue

1 diagram · 2 runnable examples (Python) · about 8 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).

Async context managers for real resources: transactions, pools and many connections Pro: opens with a Pro, Premium or Ultimate pass

An async context manager is the natural owner of anything that must be given back: a transaction, a pooled connection, a set of connections to shards. Below, each one is built and tested under errors and cancellation, and the exercise builds a limiter of your own.

In this part:

  • A transaction that rolls back on errors and on cancellation
  • A connection pool as two classes, with its limit measured
  • Many resources at once with AsyncExitStack
  • Exercise: a lazy page reader and a limit you can use with async with
  • A lazy page reader and a limit you can use with async with
    • Results of the sample tests

1 diagram · 3 runnable examples (Python) · 1 exercise with sample tests · 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).

Interview questions

  1. Mid to senior

    A streaming endpoint yields rows from an async generator that holds a database cursor. Clients often disconnect after a few rows, and under load the database runs out of connections. Why, and how do you fix it?

    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 senior

    Design an async context manager for a database transaction that commits on success, rolls back on any error and behaves correctly when the task is cancelled half-way. What must __aexit__() do in each case?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    An async generator pages through an API with 1,000 pages of 100 items, each page taking 300 ms to fetch, while the consumer needs 20 ms per item. What is held in memory, how long does the job take, and when would you put a queue between them?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An async generator, an async def with yield, awaits between items and fetches a page only when the loop asks.
  • An async comprehension builds the whole list, so a loop that stops early is an async for loop with break.
  • async for awaits __anext__() until StopAsyncIteration, async with awaits __aenter__() and __aexit__(), and Python 3.14 says when the two are swapped.
  • With asynccontextmanager, the finally after the yield gives the resource back on every way out of the block.
  • aclosing(), AsyncExitStack and a time limit around each anext() close generators and resources on time.

Terms in this lesson

async context manager
An object with the coroutine methods __aenter__() and __aexit__(). async with awaits the first before its block and the second after it, however the block ends; contextlib.asynccontextmanager makes one from an async generator.
async generator
A function defined with async def whose body contains yield, and the async iterator that calling it returns. Its body runs only while the next item is awaited, so it can await a page of data and then yield the page's items one by one.
async iterator
An object that async for can loop over: __aiter__() returns it, and each await of its __anext__() gives the next item or raises StopAsyncIteration at the end. Unlike a plain iterator, it may wait between items.
StopAsyncIteration
The exception an async iterator's __anext__() raises when no items are left. async for catches it and ends quietly, as for does with StopIteration.

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.