Skip to content

gh-137026: Add an explainer guide for asyncio #137215

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 74 commits into
base: main
Choose a base branch
from

Conversation

anordin95
Copy link

@anordin95 anordin95 commented Jul 29, 2025

Explainer guide for asyncio

gh-137026: HOWTO article for asyncio, and a reference to it from the main page of the asyncio docs.


Hi!

I've used Python's asyncio a couple times now, but never really felt confident in my mental model of how it fundamentally works and therefore how I can best leverage it. The official docs provide good documentation for each specific function in the package, but, in my opinion, are missing a cohesive overview of the systems design and architecture. Something that could help the user understand the why and how behind the recommended patterns. And a way to help the user make informed decisions about which tool in the asyncio toolkit they ought to grab, or to recognize when asyncio is the entirely wrong toolkit. I thought I'd take a stab at filling that gap and contributing back to a community that's given so much!


📚 Documentation preview 📚: https://cpython-previews--137215.org.readthedocs.build/en/137215/howto/a-conceptual-overview-of-asyncio.html

@python-cla-bot

This comment was marked as resolved.

@bedevere-app bedevere-app bot added docs Documentation in the Doc dir skip news labels Jul 29, 2025
@github-project-automation github-project-automation bot moved this to Todo in Docs PRs Jul 29, 2025
Comment on lines 148 to 150
**Unlike tasks, await-ing a coroutine does not cede control!** Wrapping a coroutine in a task first, then ``await``-ing
that would cede control. The behavior of ``await coroutine`` is effectively the same as invoking a regular,
synchronous Python function. Consider this program::
Copy link
Author

Choose a reason for hiding this comment

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

Personally, I’m not sure why this design decision was made and find it rather confuses the meaning of await: asynchronously wait. If someone here does know, please tell me so I can update this section appropriately! I think providing such context can aid significantly with peoples understanding (myself included!).

For reference, I'd imagine either having coroutines yield in their __await__ or disallowing await coroutine entirely (thereby effectively mandating people who want to use coroutines must use coroutine.send()).

Copy link
Member

Choose a reason for hiding this comment

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

If I understand your question correctly, it's just that coroutines act as a wrapper over other tasks. Using await on a coroutine will cede control once that coroutine calls await on a task.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, agreed! Coroutines will give up control once await is used on a task or future.

However, I think that coroutines not ceding on await is somewhat surprising behavior. One of the upsides of asyncio is the precise visibility into where control handoffs happen. This behavior muddles that insight a bit. The person reading the code can't just look for await's, they need to know the type of the object being awaited.

I imagine most coroutines do eventually call something which will await a task or future, but some may not. In a similar vein as that first point, this behavior feels like a sneaky gotcha that could surprise users and lead to implementations that unintentionally hog the event loop.

Finally, I don't really see a strong upside to the current behavior in the face of those downsides. I'd prefer entirely disallowing await coroutine and instead having authors rely on coroutine.send. But, of course, I might be missing something!

Copy link
Member

Choose a reason for hiding this comment

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

I imagine most coroutines do eventually call something which will await a task or future, but some may not.

It's not that intuitive, but if no future/task is called down the line, then there's no point in making it async. A function becomes async because it needs to await something else that is async, and that should go all the way down the line until you hit a case where someone made something async to await a future/task. If there isn't one, then there was no point in making anything async in the first place.

Copy link
Author

@anordin95 anordin95 Aug 4, 2025

Choose a reason for hiding this comment

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

Yes, you can still write correct or incorrect programs either way. My concern is that a well designed tool should be hard to misuse. And I don't see an upside for the synchronous await coroutine behavior, despite this downside.

For a concrete example, consider someone working in a broader async codebase. They want to create functionality to asynchronously wait for a db request from their in-house, custom db. They write something like this. And fail to realize they're unwittingly going to stall the whole event loop, despite the various uses of await.

async def read_from_db():
    while True:
        is_db_ready = await check_is_db_ready()
        if is_db_ready:
            db_record = await fetch_db_record()
            return db_record

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's basically syntactic-sugar for yield from (i.e. the syntax that was originally used in 3.4). Therefore it's needed to enter the coroutine (which is basically another name for a generator) and run that code. In other words, anything async needs to use await, but it doesn't necessarily yield.

It would be more accurate to say that await is the only place in the code where we may yield back to the event loop, but there's no guarantee unless you are familiar with the functionality of that code. e.g. In aiohttp, some users may, on the rare occasion, get caught by code processing data slower than it arrives and therefore the code never actually needs to yield to the event loop (because we're never waiting for the socket).

Copy link
Author

@anordin95 anordin95 Aug 6, 2025

Choose a reason for hiding this comment

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

Hm. @Dreamsorcerer, I'm not sure I totally follow. Let me make sure I understand!

It sounds like synchronous execution of await coroutine wasn't an explicit design goal. But moreso a side effect of building the coroutine functionality on top of yield from. Or, is there some upside to that design which I'm not seeing?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know about explicit design, I wasn't involved. But, I assume this is related to how it was implemented (i.e. using generators and yield from). I don't think tasks existed originally, so that element was added later.

The upside is probably performance, I suspect asyncio would be a lot slower if we yielded to the event loop on every single coroutine (think that a single await in your code may result in several nested awaits through the call stack before reaching a coroutine that is actually meant to yield).

We actually have micro-optimisations in aiohttp where we avoid scheduling a task if we expect it to resolve without delay.

Copy link
Author

Choose a reason for hiding this comment

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

Ahhhh!! I initially figured performance wouldn't be a huge deal, but I hadn't really thought through the extent of deeply nested tasks vs. coros. Gosh that's been bugging me for a while. Thank you :)

I set up a harness to verify. The jist of it is below. One is coroutines all the way down, the other Tasks.

async def coro5(x: int):
    return x + 1
...
async def coro2(x: int):
    return await coro3(x) + 1
async def coro1(x: int):
    return await coro2(x) + 1

async def main():
    for _ in range(10_000):
        output = await coro1(7)
    return output

Time elapsed for the coroutine approach: 0.00419s.
Time elapsed for the Task approach: 1.48859s.

https://github.com/anordin95/a-conceptual-overview-of-asyncio/blob/20fe2ddbb375e55cc9e835fa3239cfc1ffc4ec68/hypotheses/9-await-perf-coro.py#L4-L22
https://github.com/anordin95/a-conceptual-overview-of-asyncio/blob/20fe2ddbb375e55cc9e835fa3239cfc1ffc4ec68/hypotheses/10-await-perf-task.py#L4-L22

@anordin95 anordin95 changed the title - Add an explainer guide for asyncio. Add an explainer guide for asyncio [gh-137026](https://github.com/python/cpython/issues/137026) Jul 29, 2025
@anordin95 anordin95 changed the title Add an explainer guide for asyncio [gh-137026](https://github.com/python/cpython/issues/137026) Add an explainer guide for asyncio [gh-137026] Jul 29, 2025
@AA-Turner AA-Turner changed the title Add an explainer guide for asyncio [gh-137026] gh-137026: Add an explainer guide for asyncio Jul 29, 2025
Copy link
Member

@StanFromIreland StanFromIreland left a comment

Choose a reason for hiding this comment

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

I will review further once the lines are wrapped, as now it will invalidate suggestions.

Copy link
Member

@ZeroIntensity ZeroIntensity left a comment

Choose a reason for hiding this comment

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

I'm going to commit to reviewing this if nobody else has the time. I think is generally an improvement to the documentation, but definitely needs some work still. Thanks for working on this!

Copy link
Member

@StanFromIreland StanFromIreland left a comment

Choose a reason for hiding this comment

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

There are still many minor infringements of the Style guide (e.g. simple English) which could be addressed in one pass.

You can also use Sphinx cross references when discussing specific keywords, functions etc.

Copy link
Member

@ZeroIntensity ZeroIntensity left a comment

Choose a reason for hiding this comment

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

Mostly LGTM. I'm down to just typos at this point.

@willingc
Copy link
Contributor

willingc commented Aug 4, 2025

@anordin95 @ZeroIntensity I'm going to mark this request changes as a reminder to myself that I want to make sure that we address @kumaraditya303's commments sufficiently prior to any merge (as he and myself as well as a few others are listed as asyncio experts in the devguide).

Thanks for working through many of the comments and suggestions. 💪🏼

Copy link
Contributor

@willingc willingc left a comment

Choose a reason for hiding this comment

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

Marking as request changes pending @kumaraditya303's re-review.

@bedevere-app
Copy link

bedevere-app bot commented Aug 4, 2025

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.

@ZeroIntensity
Copy link
Member

Let's just use the DO-NOT-MERGE label. If I or any other core dev approves after after you hit "request changes", the bot still switches it to "awaiting merge".

Copy link
Contributor

@willingc willingc left a comment

Choose a reason for hiding this comment

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

Thanks @ZeroIntensity. Good idea.

@willingc willingc dismissed their stale review August 4, 2025 23:32

Using merge label instead.

anordin95 and others added 3 commits August 4, 2025 16:39
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
Copy link
Contributor

@willingc willingc left a comment

Choose a reason for hiding this comment

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

I've done another pass over the explainer. I've tried to read this from the perspective of someone less familiar with event loops.

I'm still on the fence about using queue, and these suggestions remove reference to the concept of queue.

Overall, this explainer is a nice addition.

@@ -38,6 +39,7 @@ Python Library Reference.

General:

* :ref:`a-conceptual-overview-of-asyncio`
Copy link
Contributor

Choose a reason for hiding this comment

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

This item should follow the argparse-tutorial. The section is alphabetized by standard library topic.

Copy link
Author

@anordin95 anordin95 Aug 5, 2025

Choose a reason for hiding this comment

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

I agree re: alphabetization. But these are rendered without the dashes and become "A Conceptual..." and "Annotations..."

Copy link
Member

Choose a reason for hiding this comment

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

I agree that we should alphabetize the rendered name.

You'll be comfortably able to answer these questions by the end of this
article:

- What's happening behind the scenes when an object is ``await``\ ed?
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- What's happening behind the scenes when an object is ``await``\ ed?
- What's happening behind the scenes when an object is awaited?

Copy link
Author

Choose a reason for hiding this comment

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

I think it was this way originally. If I recall correctly @ZeroIntensity suggested the switch. I now actually have a slight preference for awaited too, but I don't feel very strongly. Y'all should decide.

Copy link
Member

Choose a reason for hiding this comment

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

My original suggestion was to remove the dash from "await-ed". No preference from me on whether await should be in a code block.

Copy link
Author

Choose a reason for hiding this comment

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

Oh, shoot. My bad!

Turns out the only impediment was my bad memory lol. Will move the various occurrences out of code blocks 👍

Copy link
Member

Choose a reason for hiding this comment

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

I’m not a fan of this since when translating most languages will have to restructure the sentence to preserve the keyword.

Copy link
Contributor

Choose a reason for hiding this comment

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

@StanFromIreland Do you prefer keeping the backticks? I don't feel that strongly about it. If maintaining the backticks works better for translations, I'm cool with keeping them.

Copy link
Member

Choose a reason for hiding this comment

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

My apologies, my comment is not very clear. I am referring to the existing one with the backticks, since it can not be translated, and it is part of the sentence, it will not make sense in another language. The translator will either have to restructure the sentence to make it somewhat work, or juggle it around (e.g. in parenthesis) as they won't want to remove it. Your suggestion is simpler to translate.

Copy link
Contributor

Choose a reason for hiding this comment

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

So to be clear, we are removing the backticks. Correct?

Copy link
Member

Choose a reason for hiding this comment

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

(Apologies again, I should work on my clarity) Yes, I am in favour of removing them.

Comment on lines +45 to +53
In more technical terms, the event loop contains a queue of jobs to be run.
Some jobs are added directly by you, and some indirectly by :mod:`!asyncio`.
The event loop pops a job from the queue and invokes it (or "gives it control"),
similar to calling a function, and then that job runs.
Once it pauses or completes, it returns control to the event loop.
The event loop will then move on to the next job in its queue and invoke it.
This process repeats indefinitely.
Even if the queue is empty, the event loop continues to cycle (somewhat
aimlessly).
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
In more technical terms, the event loop contains a queue of jobs to be run.
Some jobs are added directly by you, and some indirectly by :mod:`!asyncio`.
The event loop pops a job from the queue and invokes it (or "gives it control"),
similar to calling a function, and then that job runs.
Once it pauses or completes, it returns control to the event loop.
The event loop will then move on to the next job in its queue and invoke it.
This process repeats indefinitely.
Even if the queue is empty, the event loop continues to cycle (somewhat
aimlessly).
Conceptually, the event loop manages jobs to be run.
Some jobs are added by you, and some indirectly by :mod:`!asyncio`.
The event loop selects a job and invokes it (or "gives it control"),
similar to calling a function, and then that job runs.
Once the job pauses or completes, it returns control to the event loop.
The event loop will then move on to the next job and invoke it.
This process repeats indefinitely.
Even if no jobs are ready to run, the event loop continues to cycle (somewhat
aimlessly).

Comment on lines +63 to +64
# This creates an event loop and indefinitely cycles through
# its queue of tasks.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# This creates an event loop and indefinitely cycles through
# its queue of tasks.
# This creates an event loop and indefinitely cycles through its available work tasks.

offers more clarity, not to mention it's somewhat cheating to use
``asyncio.sleep`` when showcasing how to implement it!

As usual, the event loop cycles through its queue of tasks, giving them control
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
As usual, the event loop cycles through its queue of tasks, giving them control
As usual, the event loop cycles through its tasks, giving them control

As usual, the event loop cycles through its queue of tasks, giving them control
and receiving control back when they pause or finish.
The ``watcher_task``, which runs the coroutine ``_sleep_watcher(...)``, will
be invoked once per full cycle of the event loop's queue.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
be invoked once per full cycle of the event loop's queue.
be invoked once per full cycle of the event loop.

Eventually, enough time will have elapsed, and ``_sleep_watcher(...)`` will
mark the future as done, and then itself finish too by breaking out of the
infinite ``while`` loop.
Given this helper task is only invoked once per cycle of the event loop's queue,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Given this helper task is only invoked once per cycle of the event loop's queue,
Given this helper task is only invoked once per cycle of the event loop,

anordin95 and others added 3 commits August 4, 2025 20:22
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
done comes from the respect and cooperation of its teammates.

In more technical terms, the event loop contains a queue of jobs to be run.
Some jobs are added directly by you, and some indirectly by :mod:`!asyncio`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm going to point out some minor clarifications, feel free to disregard if you don't think it's appropriate in these docs:

It would be more accurate here to say that any code can queue a job, including third-party libraries etc.

Comment on lines +47 to +48
The event loop pops a job from the queue and invokes it (or "gives it control"),
similar to calling a function, and then that job runs.
Copy link
Contributor

Choose a reason for hiding this comment

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

Strictly speaking, it's not quite a simple queue. Maybe this only matters to low-level asyncio developers though, and general users don't need to understand the difference.

If I remember correctly, essentially, when tasks are being added, they get added to a list of tasks to be run on the next loop cycle. Once all tasks in the current list have been iterated through once (with incomplete tasks getting re-added to the next cycle's list), only then does it move to the next loop cycle, and create a new list for a future cycle.

This is probably roughly the same as the comment below: https://github.com/python/cpython/pull/137215/files#r2250000716

Copy link
Author

Choose a reason for hiding this comment

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

Mhm! Though, they're not distinct lists/queues. The iteration uses the initial length of the queue (self._ready).

# This is the only place where callbacks are actually *called*.
# All other places just add them to ready.
# Note: We run all currently scheduled callbacks, but not any
# callbacks scheduled by callbacks run this time around --
# they will be run the next time (after another I/O poll).
# Use an idiom that is thread-safe without using locks.
ntodo = len(self._ready)
for i in range(ntodo):

Comment on lines +51 to +53
This process repeats indefinitely.
Even if the queue is empty, the event loop continues to cycle (somewhat
aimlessly).
Copy link
Contributor

Choose a reason for hiding this comment

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

Typically we use asyncio.run(), so the loop continues until that task completes, not indefinitely.

I also don't think the loop cycles aimlessly, the application would basically sleep. e.g. If all your tasks are waiting on I/O (so there are no tasks queued), then it uses the OS's select() call (on the selector event loop) to tell the OS to wake it up when it's received data on those sockets. When it wakes up, it will reschedule the tasks associated with those sockets and run another cycle.

Copy link
Author

@anordin95 anordin95 Aug 6, 2025

Choose a reason for hiding this comment

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

Ahhh, fair! Thanks :) I'll rework this a bit.

Comment on lines +65 to +66
event_loop = asyncio.new_event_loop()
event_loop.run_forever()
Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest we don't use the low-level APIs here if we're trying to teach users the correct way to utilise asyncio.

Copy link
Author

@anordin95 anordin95 Aug 6, 2025

Choose a reason for hiding this comment

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

I like the clarity that this explicit approach provides. I think it helps users gain a broader understanding. But I hear you on also showing the suggested API usage. I've added a chunk to this effect in the Tasks section:

Earlier, we manually created the event loop and set it to run forever.
In practice, it's recommended to use (and common to see) :func:asyncio.run,
which takes care of managing the event loop and ensuring the provided
coroutine finishes before advancing.
For example, many async programs follow this setup::

   import asyncio

   async def main():
       ...

   if __name__ == "__main__":
       asyncio.run(main())
       # The program will not reach the following print statement until the
       # coroutine main() finishes.
       print("coroutine main() is done!")

Copy link
Contributor

@Dreamsorcerer Dreamsorcerer Aug 6, 2025

Choose a reason for hiding this comment

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

I like the clarity that this explicit approach provides. I think it helps users gain a broader understanding.

I'll leave it to you, but personally I don't really understand what it's trying to explain.

Technically, as per my comment above, the comment here is not entirely accurate. This code in it's current state, creates an event loop and starts the first cycle of the loop and then basically sleeps indefinitely as I understand it. It does not cycle indefinitely, because there is no code that could cause it to move on to a second cycle.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like the clarity that this explicit approach provides. I think it helps users gain a broader understanding. But I hear you on also showing the suggested API usage. I've added a chunk to this effect in the Tasks section:

For new docs, you should use asyncio.run or asyncio.Runner as the runner, -1 on using older APIs which are hard to use correctly.

Copy link
Author

Choose a reason for hiding this comment

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

I'll leave it to you, but personally I don't really understand what it's trying to explain.

Mhm. The goal is to make the event loop object explicit to the user, at least just once. Then, we can relegate it to the background. Without that, the article frequently references this strange object (an event loop), but we never really get to see it as the reader. And I think the goal of this article as an explainer should be to provide some deeper context & understanding.

Yes! I hear you on the loop_forever! I'll tweak that.

For reference, the article does clarify and recommend shortly after this about using asyncio.run.

A task also maintains a list of callback functions whose importance will become
clear in a moment when we discuss ``await``.
The recommended way to create tasks is via :func:`asyncio.create_task`.
Creating a task automatically adds it to the event loop's queue of tasks.
Copy link
Contributor

Choose a reason for hiding this comment

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

I also like how mentioning the queue subtly reinforces the idea that execution is serial (i.e. not parallel).

I would additionally say that it's useful to understand that the order of execution is predictable. Many users are under the impression that "scheduled" means unpredictable order of execution. The tasks should start executing in the order they've been added.

Comment on lines +282 to +283
``await`` also does one more very special thing: it propagates (or "passes
along") any ``yield``\ s it receives up the call-chain.
Copy link
Contributor

Choose a reason for hiding this comment

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

It's essentially syntactic sugar for yield from (which was the actual syntax used in Python 3.4).

Copy link
Member

Choose a reason for hiding this comment

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

This is mentioned later.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I guess to me this should just point to yield from in the docs elsewhere, rather than re-explaining how it works. But, probably doesn't matter much.

Curiously, I don't have permissions to resolve my own comments here...

and how to make your own asynchronous operators.

================================================
coroutine.send(), await, yield and StopIteration
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's useful to understand how it works, but I'd probably rename the header to something like "How coroutines work under-the-hood".

I use similar examples for talks explaining how asyncio works, where I start by showing the more complex things that can be done with generators (that many developers are unaware of) and end up with a very simplistic approximation of an event loop, without using any async/await syntax. (I use the functions interactively in a talk, but the code examples are at: https://gist.github.com/Dreamsorcerer/5c0ddafae6eed073a90cf52fd7cfaede)

By using generators alone with no actual asyncio code though, I think I avoid misleading anybody in thinking that the syntax they see is the appropriate way to write asyncio (which I cover with a couple of points at the end with actual asyncio examples).

Comment on lines +335 to +337
The only way to yield (or effectively cede control) from a coroutine is to
``await`` an object that ``yield``\ s in its ``__await__`` method.
That might sound odd to you. You might be thinking:
Copy link
Contributor

Choose a reason for hiding this comment

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

It's probably worth an example of why this might be a problem and how to deal with it?

For example, if you await on things which don't yield or are processing some CPU-intensive data, you may need to add await asyncio.sleep(0) to the code to ensure the code does yield back to the event loop in a reasonable amount of time.

Copy link
Contributor

Choose a reason for hiding this comment

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

Additionally on that, it might be worth mentioning the debug features of asyncio, such as the ability to log anything that blocks the loop for more than (by default) 100ms.

Copy link
Author

Choose a reason for hiding this comment

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

Hm, yeah. Good callout! Lemme stew on this a bit.

Comment on lines 148 to 150
**Unlike tasks, await-ing a coroutine does not cede control!** Wrapping a coroutine in a task first, then ``await``-ing
that would cede control. The behavior of ``await coroutine`` is effectively the same as invoking a regular,
synchronous Python function. Consider this program::
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's basically syntactic-sugar for yield from (i.e. the syntax that was originally used in 3.4). Therefore it's needed to enter the coroutine (which is basically another name for a generator) and run that code. In other words, anything async needs to use await, but it doesn't necessarily yield.

It would be more accurate to say that await is the only place in the code where we may yield back to the event loop, but there's no guarantee unless you are familiar with the functionality of that code. e.g. In aiohttp, some users may, on the rare occasion, get caught by code processing data slower than it arrives and therefore the code never actually needs to yield to the event loop (because we're never waiting for the socket).

@kumaraditya303
Copy link
Contributor

I'm still on the fence about using queue, and these suggestions remove reference to the concept of queue.

I agree, using queue as the terminology here isn't completely correct so +1 to remove it.

::

class YieldToEventLoop:
def __await__(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Bare yield is used internally by asyncio to implement asyncio.sleep(0) and is special cased, I don't think this is a good example to document as it relies too much on implementation detail.

Copy link
Author

@anordin95 anordin95 Aug 6, 2025

Choose a reason for hiding this comment

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

Hm. I don't think this is an implementation detail of asyncio. If we momentarily ignore asyncio completely, this (yielding from an __await__) is the only way to cede control from a Python coroutine. Could you say more about it being special cased?

Instead, I think asyncio.sleep(0), this example and any other async library likely use this same fundamental pattern exposed by Python.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm. I don't think this is an implementation detail of asyncio.

Why? Is this documented somewhere?

Could you say more about it being special cased?

yielding None is special cased and is used for relinquishing control for one event loop iteration. You can see this in implementation of tasks.

Instead, I think asyncio.sleep(0), this example and any other async library likely use this same fundamental pattern exposed by Python.

No, asyncio libraries are built on top of futures and tasks and not objects implementing __await__ from scratch.

Copy link
Contributor

Choose a reason for hiding this comment

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

No, asyncio libraries are built on top of futures and tasks and not objects implementing __await__ from scratch.

I think they mean alternatives to asyncio such as trio, not asyncio libraries. The point is to understand how asyncio actually works, rather than the bare minimum needed to build something on top of it. Understanding this point helps advanced users understand when (and why) code may or may not yield to the event loop.

Copy link
Author

@anordin95 anordin95 Aug 6, 2025

Choose a reason for hiding this comment

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

Why? Is this documented somewhere?

yielding None is special cased and is used for relinquishing control for one event loop iteration. You can see this in implementation of tasks.

I'm not sure! I haven't checked the docs. And yes I imagine it is! But that's not really my point. asyncio happens to use yielding from an __await__ to cede control. But, that's also the only way to cede control from a coroutine. This is illustrated in the example earlier in the article which features no usage of asyncio nor an event loop.

Comment on lines +164 to +165
The recommended way to create tasks is via :func:`asyncio.create_task`.
Creating a task automatically adds it to the event loop's queue of tasks.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
The recommended way to create tasks is via :func:`asyncio.create_task`.
Creating a task automatically adds it to the event loop's queue of tasks.
The recommended way to create tasks is via :func:`asyncio.create_task`
which creates the task and schedules its execution by the event loop.

In practice, it's slightly more complex, but not by much.
In part 2, we'll walk through the details that make this possible.

**Unlike tasks, awaiting a coroutine does not hand control back to the event
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't entirely correct, using tasks does not guarantee that the control will be given to event loop if e.g. the task completes eagerly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Todo
Development

Successfully merging this pull request may close these issues.

9 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy