Kick off your book project in 3 hours! Live workshop on Zoom. You’ll leave with a real book project, progress on your first chapter, and a clear plan to keep going. Saturday, May 16, 2026. Learn more…

Leanpub Header

Skip to main content

asyncio from ground up

A working Mental Model for Python asyncio

Most asyncio tutorials introduce async and await on page one and ask you to take the runtime on faith. This book does it the other way around. You build a working event loop in

thirty lines of plain Python — no asyncio import — and meet the keywords as labels for parts of a runtime you have already watched run. By the end, async Python stops being

intimidating and starts being readable.

Minimum price

$14.99

$17.99

You pay

$17.99

Author earns

$14.39
$
You can also buy this book with 1 book credit. Get book credits with a Reader Membership or an Organization Membership for your team.
PDF
EPUB
WEB
About

About

About the Book

You have probably tried asyncio once and given up. You may have searched "python asyncio tutorial" and bounced off three of them, or watched senior engineers go quiet the moment `await` appears in a code review. None of those experiences is unusual, and none of them is your fault.

The reason most readers find asyncio confusing is that the topic is taught keywords-first. The first page of a typical tutorial introduces `async def`, the second introduces `await`, and the reader is expected to imagine the runtime underneath. A keyword without a runtime is a name without a thing. The reader, fairly, gives up. After a few rounds of giving up, async code becomes "the topic I avoid", and the avoidance hardens into a wall.

This book is that wall coming down.

THE APPROACH

asyncio is small. A loop that runs the next ready job. A way to pause a function in the middle and resume it later. A handful of keywords that name parts of that mechanism. The difficulty is in the order topics are usually taught. We do them in the opposite order.

Part I (Chapters 1 to 2) names the felt problem. A blocking script that freezes for twelve seconds while it fetches six URLs one after another. The threaded version that helps but does not scale.

Part II (Chapters 3 to 6) builds the runtime in plain Python. Chapter 3 hand-rolls a working event loop in 30 lines, no asyncio import in sight. Chapters 4 and 5 introduce `async`, `await`, and `asyncio.run` as labels for parts of a runtime you have already watched run. Chapter 6 takes the lid off at the bytecode level for readers who want depth.

Part III (Chapters 7 to 10) is the production toolbox: `gather`, `create_task`, `TaskGroup`, the time toolbox, async iteration, structured concurrency, cancellation, exception groups, sync primitives, and `contextvars`.

Part IV (Chapters 11 to 12) takes you to production. Chapter 11 is a guided debugging session for the most common asyncio failure modes. Chapter 12 covers testing async code with `pytest-asyncio` and `AsyncMock`, including the silent-pass bug that makes broken tests look green.

Part V (Chapter 13) is where asyncio ends. Executors, sync↔async bridges, graceful shutdown on SIGINT and SIGTERM.

THE RUNNING EXAMPLES

A single CLI script, `news.py`, evolves chapter by chapter from a blocking version that takes twelve seconds into a production-shaped program with structured concurrency, semaphore-capped fan-out, trace IDs, and graceful shutdown. A parallel example, `tcpcheck.py`, applies the same asyncio primitives to raw TCP. You leave with two working scripts you could ship.

WHO THIS BOOK IS FOR

Python developers who have been avoiding asyncio. Backend engineers writing services that talk to APIs, databases, and queues. CLI authors making scripts that fetch from multiple sources. Anyone who has read about the GIL and come away thinking Python is broken for concurrent programming.

If you are building GenAI applications, this book is essential. Every LLM-powered system is I/O orchestration at heart: parallel calls to a model provider, streaming tokens back to clients, fan-out to embeddings and vector searches, rate-limited retries, agent loops that keep WebSockets open while tool calls resolve. asyncio is what makes that orchestration scalable and performant. A GenAI service written without asyncio leaves most of its throughput on the table.

You should already know functions, classes, imports, and how to read a stack trace. You do not need to know what an event loop is, what `__await__` does, or how `epoll` works underneath the loop. The book teaches all three.

WHAT YOU WILL LEAVE WITH

You will read async Python without flinching. The line that is a keyword and the line that is doing real work will be obvious to you at a glance. You will write your own async programs, recognize when asyncio is the wrong tool, and reach for the right one instead. asyncio's place in a design conversation will be a question you can answer rather than a question you change the subject to.

The technology is small. The barrier was the order in which it has been taught to you. Time to do it the other way around.

Author

About the Author

Ritesh Modi

Ritesh Modi is Head of AI at MarketOnce and a former Forward Deployed Engineer at Microsoft. He has spent more than a decade building and shipping production systems across cloud, distributed computing, and applied machine learning, working with organizations ranging from global enterprises to fast-moving startups. His recent work focuses on applied large language models, designing systems that turn pretrained models into reliable, task-specific tools.

Ritesh has authored multiple technology books and speaks regularly at industry conferences on AI, cloud architecture, and software engineering. His writing philosophy rests on a simple belief: the best technical books are written by practitioners who still remember what it felt like to not understand something, not by experts who have forgotten. Every explanation in this book was tested against that standard, if it would not have made sense to him when he was first learning this material, it was rewritten until it did.

He writes, shares ideas, and connects with readers at www.riteshmodi.com. When he is not writing or building AI systems, he can be found mentoring engineers, exploring new architectures, or debugging a training run that should have converged three hours ago.

Contents

Table of Contents

Getting Started

  1. Unsure How to Get Started? Try our Book Workshop!
  2. How to Write on Leanpub
  3. Previewing and publishing
  4. Basic formatting
  5. Markdown and Markua
  6. Generate a preview version of your book
  7. Either read a tutorial, or just go for it!
  8. Thanks for being a Leanpub author!

Writing in Markua

  1. Section One
  2. Including a Chapter in the Sample Book
  3. Links
  4. Images
  5. Lists
  6. Page Breaks
  7. Code Samples
  8. Tables
  9. Math
  10. Headings
  11. Block quotes, Asides and Blurbs
  12. Good luck, have fun!

From the Author

Preface

What This Book Is About

What This Book Is Not

Who This Book Is For

How to Use This Book

Conventions Used in This Book

Companion Code and Data

How to Contact Us

Acknowledgments

Contents

The Blocking Function

  1. 1.1 The first version of news.py
  2. 1.2 What “blocking” actually means
  3. 1.3 The cost grows with the list
  4. 1.4 The same shape, one layer down
  5. 1.5 The question that drives the rest of the book
  6. Summary

Concurrency, Parallelism, and the GIL

  1. 2.1 What a thread is, briefly
  2. 2.2 The threaded version of news.py
  3. 2.3 Threading the TCP probes
  4. 2.4 Why the speedup actually works (and the GIL story you have heard wrong)
  5. 2.5 Why threads are not the answer the book is reaching for
  6. 2.6 The two scales the book cares about
  7. 2.7 Where the next chapter goes
  8. Summary

The Loop, From Scratch

  1. 3.1 A queue of jobs
  2. 3.2 A loop that runs the next ready job
  3. 3.3 Two senses of “ready”, and the generator that gives us pause
  4. 3.4 The loop sleeps when nothing is ready
  5. 3.5 Connecting the toy loop to news.py
  6. Summary

Coroutines, Without the Magic

  1. 4.1 Generators, the original coroutine
  2. 4.2 yield as a pause point, one more time
  3. 4.3 The toy loop
  4. 4.4 What async def returns
  5. Polite driver for x in gen(): (no for; the loop handles it)
  6. 4.5 Coroutines are not running until the loop runs them
  7. Summary

async and await

  1. 5.1 What async does to a function definition
  2. 5.2 What await does at a call site
  3. 5.3 What can be awaited, and what cannot
  4. 5.4 The first real asyncio news.py
  5. 5.5 The cliffhanger
  6. 5.6 The same keywords, on raw TCP
  7. Summary

await Yields, Suspends, and Resumes

  1. 6.1 What await does
  2. 6.2 What is saved across the pause
  3. 6.3 How the loop wakes the coroutine back up
  4. 6.4 A Task is a coroutine that the loop is driving
  5. 6.5 For the curious: coro.send(value) and the bytecode
  6. 6.6 For the curious: selectors and the operating system
  7. 6.7 Watching news.py yield and resume
  8. Summary

asyncio.run, create_task, gather

  1. 7.1 asyncio.run as the starting line
  2. 7.2 What asyncio.run does
  3. 7.3 await coro is sequential; create_task(coro) is concurrent
  4. 7.4 asyncio.gather for “run these together”
  5. 7.5 The fast news.py
  6. 7.6 Concurrent TCP probes
  7. 7.7 The lost-task trap
  8. Summary

The Time Toolbox

  1. 8.1 asyncio.sleep yields; time.sleep blocks
  2. 8.2 wait_for for a deadline around an awaitable
  3. 8.3 asyncio.timeout() as a context manager (Python 3.11+)
  4. 8.4 asyncio.shield to protect from cancellation
  5. 8.5 asyncio.wait vs asyncio.gather
  6. 8.6 asyncio.as_completed for results as they arrive
  7. 8.7 Timeouts and streaming
  8. Summary

async for, async with, Async Iteration and Resource Management

  1. 9.1 async for and the iterator protocol
  2. 9.2 Async generators
  3. 9.3 async for over aiohttp response bodies
  4. 9.4 async with and the context-manager protocol
  5. 9.5 Writing your own async context manager
  6. 9.6 The streaming news.py
  7. Summary

Tasks, TaskGroups, Cancellation, Exception Groups, and contextvars

  1. 10.1 Task is the loop’s unit of work; the coroutine is just the recipe
  2. 10.2 asyncio.TaskGroup: structured concurrency by construction
  3. 10.3 Cancellation: task.cancel() and CancelledError at the next await
  4. 10.4 The bare-except trap
  5. 10.5 ExceptionGroup and except*
  6. 10.6 Re-raising CancelledError after cleanup
  7. 10.7 Canceling a TCP probe
  8. 10.8 Synchronization primitives
  9. 10.9 Capping concurrency with Semaphore: 50 sites, 10 in flight
  10. 10.10 contextvars for task-scoped state
  11. 10.11 Producer-consumer with asyncio.Queue
  12. Summary

What Goes Wrong, and How to Find It

  1. 11.1 The failure mode: one synchronous call freezes the whole loop
  2. 11.2 The blocking patterns to learn to recognize
  3. 11.3 The single most useful tool: asyncio’s debug mode
  4. 11.4 The four warnings the runtime emits
  5. 11.5 asyncio.all_tasks() and task.get_stack()
  6. 11.6 aiomonitor: a live REPL inside a running program
  7. 11.7 Profiling that sees the loop correctly
  8. 11.8 Logging task names
  9. 11.9 pdb and async
  10. 11.10 A guided debugging session on the broken news.py
  11. 11.11 tcpcheck.py as a diagnostic tool
  12. Summary

Testing async code

  1. 12.1 Your first async test
  2. 12.2 The standard-library alternative
  3. 12.3 What mocks are, and why async needs a different one
  4. 12.4 Testing cancellation
  5. 12.5 Optional: Testing TaskGroup error propagation
  6. 12.6 Event loop fixture scope
  7. 12.7 Testing timeouts without real time
  8. 12.8 The most subtle async test bug
  9. 12.9 When a test hangs
  10. 12.10 The full test suite for the fetcher
  11. Summary

Where asyncio Ends: Executors, Graceful Shutdown

  1. 13.1 Fixing the parser bug from Chapter 11
  2. 13.2 The executor toolbox
  3. 13.3 The decision tree
  4. Heavy CPU work that needs to scale across cores Use multiprocessing directly. asyncio is the wrong tool for that workload.
  5. 13.4 Bridging from sync to async, and back
  6. 13.5 Library design: expose async, do not hide it
  7. 13.6 Graceful shutdown
  8. 13.7 The final news.py
  9. Summary

Appendix A: Setting up your environment

  1. A.1 Python versions and what they buy you
  2. A.2 The packages the book uses
  3. A.3 The smallest runnable asyncio script
  4. A.4 Reading the running example as you go

Appendix B: The asyncio API surface

  1. Functions
  2. asyncio.wait_for(coro, timeout) Wraps a single awaitable with a deadline; cancels the inner work and raises TimeoutError on expiry. 8
  3. Classes and protocols
  4. ExceptionGroup The exception type that holds multiple exceptions raised by sibling Tasks in a TaskGroup. Caught with except* (3.11+). 10
  5. Loop methods worth knowing
  6. loop.set_default_executor(executor) Replaces the loop’s default executor. Useful when the default thread pool is too small for the workload. 13
  7. aiohttp API surface used in the book
  8. response.text() Awaitable that returns the full response body as a decoded string. 5
  9. Where to find the rest

Appendix C: Troubleshooting guide

  1. C.1 Asyncio runtime warnings, what they mean and what to fix
  2. RuntimeWarning: async generator 'X' was never closed An async generator was garbage-collected mid-iteration without the consumer running it to completion or calling aclose(). Wrap resources held between yield in try/finally. Call aclose() explicitly when you need cleanup at a specific moment. (Chapter 9, Section 9.2.1.)
  3. C.2 The script runs without errors but is no faster than synchronous
  4. Slow-callback warnings fire (Executing took 0.234 seconds) when running with debug=True. A coroutine ran for too long between await, blocking the loop. Usually a CPU-bound function or a sync I/O call. Move the slow work to a thread pool with asyncio.to_thread. For CPU-bound work that needs cores, use a process pool via loop.run_in_executor(ProcessPoolExecutor(), ...). (Chapter 11; Chapter 13.)
  5. C.3 The program hangs or never exits
  6. Ctrl+C does not interrupt the program. No signal handler installed; or a signal handler that schedules async work but asyncio.run is blocked elsewhere. Install signal handlers with loop.add_signal_handler(sig, callback). Cancel the top-level Task in the callback. The TaskGroup propagates cancellation to children. (Chapter 13.)
  7. C.4 Common errors and what they mean
  8. RuntimeError: Event loop is closed (often during program shutdown) Code is trying to use the loop after asyncio.run returned and closed it. Common when a finalizer or __del__ method tries to schedule async work. Move any async cleanup into the running coroutine’s normal exit path (finally blocks, __aexit__). Do not rely on __del__ or atexit for async cleanup.
  9. When the table does not have your symptom

Appendix D: Further reading

  1. D.1 The asyncio source code
  2. D.2 The async library ecosystem
  3. D.3 The structured concurrency literature
  4. D.4 The PEPs that shaped asyncio
  5. D.5 Things this book did not cover, that you may want next

Glossary

About the Author

From the Author

Preface

What This Book Is About

What This Book Is Not

Who This Book Is For

How to Use This Book

Conventions Used in This Book

Companion Code and Data

How to Contact Us

Acknowledgments

Contents

The Blocking Function

  1. 1.1 The first version of news.py
  2. 1.2 What “blocking” actually means
  3. 1.3 The cost grows with the list
  4. 1.4 The same shape, one layer down
  5. 1.5 The question that drives the rest of the book
  6. Summary

Concurrency, Parallelism, and the GIL

  1. 2.1 What a thread is, briefly
  2. 2.2 The threaded version of news.py
  3. 2.3 Threading the TCP probes
  4. 2.4 Why the speedup actually works (and the GIL story you have heard wrong)
  5. 2.5 Why threads are not the answer the book is reaching for
  6. 2.6 The two scales the book cares about
  7. 2.7 Where the next chapter goes
  8. Summary

The Loop, From Scratch

  1. 3.1 A queue of jobs
  2. 3.2 A loop that runs the next ready job
  3. 3.3 Two senses of “ready”, and the generator that gives us pause
  4. 3.4 The loop sleeps when nothing is ready
  5. 3.5 Connecting the toy loop to news.py
  6. Summary

Coroutines, Without the Magic

  1. 4.1 Generators, the original coroutine
  2. 4.2 yield as a pause point, one more time
  3. 4.3 The toy loop
  4. 4.4 What async def returns
  5. Polite driver for x in gen(): (no for; the loop handles it)
  6. 4.5 Coroutines are not running until the loop runs them
  7. Summary

async and await

  1. 5.1 What async does to a function definition
  2. 5.2 What await does at a call site
  3. 5.3 What can be awaited, and what cannot
  4. 5.4 The first real asyncio news.py
  5. 5.5 The cliffhanger
  6. 5.6 The same keywords, on raw TCP
  7. Summary

await Yields, Suspends, and Resumes

  1. 6.1 What await does
  2. 6.2 What is saved across the pause
  3. 6.3 How the loop wakes the coroutine back up
  4. 6.4 A Task is a coroutine that the loop is driving
  5. 6.5 For the curious: coro.send(value) and the bytecode
  6. 6.6 For the curious: selectors and the operating system
  7. 6.7 Watching news.py yield and resume
  8. Summary

asyncio.run, create_task, gather

  1. 7.1 asyncio.run as the starting line
  2. 7.2 What asyncio.run does
  3. 7.3 await coro is sequential; create_task(coro) is concurrent
  4. 7.4 asyncio.gather for “run these together”
  5. 7.5 The fast news.py
  6. 7.6 Concurrent TCP probes
  7. 7.7 The lost-task trap
  8. Summary

The Time Toolbox

  1. 8.1 asyncio.sleep yields; time.sleep blocks
  2. 8.2 wait_for for a deadline around an awaitable
  3. 8.3 asyncio.timeout() as a context manager (Python 3.11+)
  4. 8.4 asyncio.shield to protect from cancellation
  5. 8.5 asyncio.wait vs asyncio.gather
  6. 8.6 asyncio.as_completed for results as they arrive
  7. 8.7 Timeouts and streaming
  8. Summary

async for, async with, Async Iteration and Resource Management

  1. 9.1 async for and the iterator protocol
  2. 9.2 Async generators
  3. 9.3 async for over aiohttp response bodies
  4. 9.4 async with and the context-manager protocol
  5. 9.5 Writing your own async context manager
  6. 9.6 The streaming news.py
  7. Summary

Tasks, TaskGroups, Cancellation, Exception Groups, and contextvars

  1. 10.1 Task is the loop’s unit of work; the coroutine is just the recipe
  2. 10.2 asyncio.TaskGroup: structured concurrency by construction
  3. 10.3 Cancellation: task.cancel() and CancelledError at the next await
  4. 10.4 The bare-except trap
  5. 10.5 ExceptionGroup and except*
  6. 10.6 Re-raising CancelledError after cleanup
  7. 10.7 Canceling a TCP probe
  8. 10.8 Synchronization primitives
  9. 10.9 Capping concurrency with Semaphore: 50 sites, 10 in flight
  10. 10.10 contextvars for task-scoped state
  11. 10.11 Producer-consumer with asyncio.Queue
  12. Summary

What Goes Wrong, and How to Find It

  1. 11.1 The failure mode: one synchronous call freezes the whole loop
  2. 11.2 The blocking patterns to learn to recognize
  3. 11.3 The single most useful tool: asyncio’s debug mode
  4. 11.4 The four warnings the runtime emits
  5. 11.5 asyncio.all_tasks() and task.get_stack()
  6. 11.6 aiomonitor: a live REPL inside a running program
  7. 11.7 Profiling that sees the loop correctly
  8. 11.8 Logging task names
  9. 11.9 pdb and async
  10. 11.10 A guided debugging session on the broken news.py
  11. 11.11 tcpcheck.py as a diagnostic tool
  12. Summary

Testing async code

  1. 12.1 Your first async test
  2. 12.2 The standard-library alternative
  3. 12.3 What mocks are, and why async needs a different one
  4. 12.4 Testing cancellation
  5. 12.5 Optional: Testing TaskGroup error propagation
  6. 12.6 Event loop fixture scope
  7. 12.7 Testing timeouts without real time
  8. 12.8 The most subtle async test bug
  9. 12.9 When a test hangs
  10. 12.10 The full test suite for the fetcher
  11. Summary

Where asyncio Ends: Executors, Graceful Shutdown

  1. 13.1 Fixing the parser bug from Chapter 11
  2. 13.2 The executor toolbox
  3. 13.3 The decision tree
  4. Heavy CPU work that needs to scale across cores Use multiprocessing directly. asyncio is the wrong tool for that workload.
  5. 13.4 Bridging from sync to async, and back
  6. 13.5 Library design: expose async, do not hide it
  7. 13.6 Graceful shutdown
  8. 13.7 The final news.py
  9. Summary

Appendix A: Setting up your environment

  1. A.1 Python versions and what they buy you
  2. A.2 The packages the book uses
  3. A.3 The smallest runnable asyncio script
  4. A.4 Reading the running example as you go

Appendix B: The asyncio API surface

  1. Functions
  2. asyncio.wait_for(coro, timeout) Wraps a single awaitable with a deadline; cancels the inner work and raises TimeoutError on expiry. 8
  3. Classes and protocols
  4. ExceptionGroup The exception type that holds multiple exceptions raised by sibling Tasks in a TaskGroup. Caught with except* (3.11+). 10
  5. Loop methods worth knowing
  6. loop.set_default_executor(executor) Replaces the loop’s default executor. Useful when the default thread pool is too small for the workload. 13
  7. aiohttp API surface used in the book
  8. response.text() Awaitable that returns the full response body as a decoded string. 5
  9. Where to find the rest

Appendix C: Troubleshooting guide

  1. C.1 Asyncio runtime warnings, what they mean and what to fix
  2. RuntimeWarning: async generator 'X' was never closed An async generator was garbage-collected mid-iteration without the consumer running it to completion or calling aclose(). Wrap resources held between yield in try/finally. Call aclose() explicitly when you need cleanup at a specific moment. (Chapter 9, Section 9.2.1.)
  3. C.2 The script runs without errors but is no faster than synchronous
  4. Slow-callback warnings fire (Executing took 0.234 seconds) when running with debug=True. A coroutine ran for too long between await, blocking the loop. Usually a CPU-bound function or a sync I/O call. Move the slow work to a thread pool with asyncio.to_thread. For CPU-bound work that needs cores, use a process pool via loop.run_in_executor(ProcessPoolExecutor(), ...). (Chapter 11; Chapter 13.)
  5. C.3 The program hangs or never exits
  6. Ctrl+C does not interrupt the program. No signal handler installed; or a signal handler that schedules async work but asyncio.run is blocked elsewhere. Install signal handlers with loop.add_signal_handler(sig, callback). Cancel the top-level Task in the callback. The TaskGroup propagates cancellation to children. (Chapter 13.)
  7. C.4 Common errors and what they mean
  8. RuntimeError: Event loop is closed (often during program shutdown) Code is trying to use the loop after asyncio.run returned and closed it. Common when a finalizer or __del__ method tries to schedule async work. Move any async cleanup into the running coroutine’s normal exit path (finally blocks, __aexit__). Do not rely on __del__ or atexit for async cleanup.
  9. When the table does not have your symptom

Appendix D: Further reading

  1. D.1 The asyncio source code
  2. D.2 The async library ecosystem
  3. D.3 The structured concurrency literature
  4. D.4 The PEPs that shaped asyncio
  5. D.5 Things this book did not cover, that you may want next

Glossary

About the Author

From the Author

Preface

What This Book Is About

What This Book Is Not

Who This Book Is For

How to Use This Book

Conventions Used in This Book

Companion Code and Data

How to Contact Us

Acknowledgments

Contents

The Blocking Function

  1. 1.1 The first version of news.py
  2. 1.2 What “blocking” actually means
  3. 1.3 The cost grows with the list
  4. 1.4 The same shape, one layer down
  5. 1.5 The question that drives the rest of the book
  6. Summary

Concurrency, Parallelism, and the GIL

  1. 2.1 What a thread is, briefly
  2. 2.2 The threaded version of news.py
  3. 2.3 Threading the TCP probes
  4. 2.4 Why the speedup actually works (and the GIL story you have heard wrong)
  5. 2.5 Why threads are not the answer the book is reaching for
  6. 2.6 The two scales the book cares about
  7. 2.7 Where the next chapter goes
  8. Summary

The Loop, From Scratch

  1. 3.1 A queue of jobs
  2. 3.2 A loop that runs the next ready job
  3. 3.3 Two senses of “ready”, and the generator that gives us pause
  4. 3.4 The loop sleeps when nothing is ready
  5. 3.5 Connecting the toy loop to news.py
  6. Summary

Coroutines, Without the Magic

  1. 4.1 Generators, the original coroutine
  2. 4.2 yield as a pause point, one more time
  3. 4.3 The toy loop
  4. 4.4 What async def returns
  5. Polite driver for x in gen(): (no for; the loop handles it)
  6. 4.5 Coroutines are not running until the loop runs them
  7. Summary

async and await

  1. 5.1 What async does to a function definition
  2. 5.2 What await does at a call site
  3. 5.3 What can be awaited, and what cannot
  4. 5.4 The first real asyncio news.py
  5. 5.5 The cliffhanger
  6. 5.6 The same keywords, on raw TCP
  7. Summary

await Yields, Suspends, and Resumes

  1. 6.1 What await does
  2. 6.2 What is saved across the pause
  3. 6.3 How the loop wakes the coroutine back up
  4. 6.4 A Task is a coroutine that the loop is driving
  5. 6.5 For the curious: coro.send(value) and the bytecode
  6. 6.6 For the curious: selectors and the operating system
  7. 6.7 Watching news.py yield and resume
  8. Summary

asyncio.run, create_task, gather

  1. 7.1 asyncio.run as the starting line
  2. 7.2 What asyncio.run does
  3. 7.3 await coro is sequential; create_task(coro) is concurrent
  4. 7.4 asyncio.gather for “run these together”
  5. 7.5 The fast news.py
  6. 7.6 Concurrent TCP probes
  7. 7.7 The lost-task trap
  8. Summary

The Time Toolbox

  1. 8.1 asyncio.sleep yields; time.sleep blocks
  2. 8.2 wait_for for a deadline around an awaitable
  3. 8.3 asyncio.timeout() as a context manager (Python 3.11+)
  4. 8.4 asyncio.shield to protect from cancellation
  5. 8.5 asyncio.wait vs asyncio.gather
  6. 8.6 asyncio.as_completed for results as they arrive
  7. 8.7 Timeouts and streaming
  8. Summary

async for, async with, Async Iteration and Resource Management

  1. 9.1 async for and the iterator protocol
  2. 9.2 Async generators
  3. 9.3 async for over aiohttp response bodies
  4. 9.4 async with and the context-manager protocol
  5. 9.5 Writing your own async context manager
  6. 9.6 The streaming news.py
  7. Summary

Tasks, TaskGroups, Cancellation, Exception Groups, and contextvars

  1. 10.1 Task is the loop’s unit of work; the coroutine is just the recipe
  2. 10.2 asyncio.TaskGroup: structured concurrency by construction
  3. 10.3 Cancellation: task.cancel() and CancelledError at the next await
  4. 10.4 The bare-except trap
  5. 10.5 ExceptionGroup and except*
  6. 10.6 Re-raising CancelledError after cleanup
  7. 10.7 Canceling a TCP probe
  8. 10.8 Synchronization primitives
  9. 10.9 Capping concurrency with Semaphore: 50 sites, 10 in flight
  10. 10.10 contextvars for task-scoped state
  11. 10.11 Producer-consumer with asyncio.Queue
  12. Summary

What Goes Wrong, and How to Find It

  1. 11.1 The failure mode: one synchronous call freezes the whole loop
  2. 11.2 The blocking patterns to learn to recognize
  3. 11.3 The single most useful tool: asyncio’s debug mode
  4. 11.4 The four warnings the runtime emits
  5. 11.5 asyncio.all_tasks() and task.get_stack()
  6. 11.6 aiomonitor: a live REPL inside a running program
  7. 11.7 Profiling that sees the loop correctly
  8. 11.8 Logging task names
  9. 11.9 pdb and async
  10. 11.10 A guided debugging session on the broken news.py
  11. 11.11 tcpcheck.py as a diagnostic tool
  12. Summary

Testing async code

  1. 12.1 Your first async test
  2. 12.2 The standard-library alternative
  3. 12.3 What mocks are, and why async needs a different one
  4. 12.4 Testing cancellation
  5. 12.5 Optional: Testing TaskGroup error propagation
  6. 12.6 Event loop fixture scope
  7. 12.7 Testing timeouts without real time
  8. 12.8 The most subtle async test bug
  9. 12.9 When a test hangs
  10. 12.10 The full test suite for the fetcher
  11. Summary

Where asyncio Ends: Executors, Graceful Shutdown

  1. 13.1 Fixing the parser bug from Chapter 11
  2. 13.2 The executor toolbox
  3. 13.3 The decision tree
  4. Heavy CPU work that needs to scale across cores Use multiprocessing directly. asyncio is the wrong tool for that workload.
  5. 13.4 Bridging from sync to async, and back
  6. 13.5 Library design: expose async, do not hide it
  7. 13.6 Graceful shutdown
  8. 13.7 The final news.py
  9. Summary

Appendix A: Setting up your environment

  1. A.1 Python versions and what they buy you
  2. A.2 The packages the book uses
  3. A.3 The smallest runnable asyncio script
  4. A.4 Reading the running example as you go

Appendix B: The asyncio API surface

  1. Functions
  2. asyncio.wait_for(coro, timeout) Wraps a single awaitable with a deadline; cancels the inner work and raises TimeoutError on expiry. 8
  3. Classes and protocols
  4. ExceptionGroup The exception type that holds multiple exceptions raised by sibling Tasks in a TaskGroup. Caught with except* (3.11+). 10
  5. Loop methods worth knowing
  6. loop.set_default_executor(executor) Replaces the loop’s default executor. Useful when the default thread pool is too small for the workload. 13
  7. aiohttp API surface used in the book
  8. response.text() Awaitable that returns the full response body as a decoded string. 5
  9. Where to find the rest

Appendix C: Troubleshooting guide

  1. C.1 Asyncio runtime warnings, what they mean and what to fix
  2. RuntimeWarning: async generator 'X' was never closed An async generator was garbage-collected mid-iteration without the consumer running it to completion or calling aclose(). Wrap resources held between yield in try/finally. Call aclose() explicitly when you need cleanup at a specific moment. (Chapter 9, Section 9.2.1.)
  3. C.2 The script runs without errors but is no faster than synchronous
  4. Slow-callback warnings fire (Executing took 0.234 seconds) when running with debug=True. A coroutine ran for too long between await, blocking the loop. Usually a CPU-bound function or a sync I/O call. Move the slow work to a thread pool with asyncio.to_thread. For CPU-bound work that needs cores, use a process pool via loop.run_in_executor(ProcessPoolExecutor(), ...). (Chapter 11; Chapter 13.)
  5. C.3 The program hangs or never exits
  6. Ctrl+C does not interrupt the program. No signal handler installed; or a signal handler that schedules async work but asyncio.run is blocked elsewhere. Install signal handlers with loop.add_signal_handler(sig, callback). Cancel the top-level Task in the callback. The TaskGroup propagates cancellation to children. (Chapter 13.)
  7. C.4 Common errors and what they mean
  8. RuntimeError: Event loop is closed (often during program shutdown) Code is trying to use the loop after asyncio.run returned and closed it. Common when a finalizer or __del__ method tries to schedule async work. Move any async cleanup into the running coroutine’s normal exit path (finally blocks, __aexit__). Do not rely on __del__ or atexit for async cleanup.
  9. When the table does not have your symptom

Appendix D: Further reading

  1. D.1 The asyncio source code
  2. D.2 The async library ecosystem
  3. D.3 The structured concurrency literature
  4. D.4 The PEPs that shaped asyncio
  5. D.5 Things this book did not cover, that you may want next

Glossary

About the Author

From the Author

Preface

What This Book Is About

What This Book Is Not

Who This Book Is For

How to Use This Book

Conventions Used in This Book

Companion Code and Data

How to Contact Us

Acknowledgments

Contents

The Blocking Function

  1. 1.1 The first version of news.py
  2. 1.2 What “blocking” actually means
  3. 1.3 The cost grows with the list
  4. 1.4 The same shape, one layer down
  5. 1.5 The question that drives the rest of the book
  6. Summary

Concurrency, Parallelism, and the GIL

  1. 2.1 What a thread is, briefly
  2. 2.2 The threaded version of news.py
  3. 2.3 Threading the TCP probes
  4. 2.4 Why the speedup actually works (and the GIL story you have heard wrong)
  5. 2.5 Why threads are not the answer the book is reaching for
  6. 2.6 The two scales the book cares about
  7. 2.7 Where the next chapter goes
  8. Summary

The Loop, From Scratch

  1. 3.1 A queue of jobs
  2. 3.2 A loop that runs the next ready job
  3. 3.3 Two senses of “ready”, and the generator that gives us pause
  4. 3.4 The loop sleeps when nothing is ready
  5. 3.5 Connecting the toy loop to news.py
  6. Summary

Coroutines, Without the Magic

  1. 4.1 Generators, the original coroutine
  2. 4.2 yield as a pause point, one more time
  3. 4.3 The toy loop
  4. 4.4 What async def returns
  5. Polite driver for x in gen(): (no for; the loop handles it)
  6. 4.5 Coroutines are not running until the loop runs them
  7. Summary

async and await

  1. 5.1 What async does to a function definition
  2. 5.2 What await does at a call site
  3. 5.3 What can be awaited, and what cannot
  4. 5.4 The first real asyncio news.py
  5. 5.5 The cliffhanger
  6. 5.6 The same keywords, on raw TCP
  7. Summary

await Yields, Suspends, and Resumes

  1. 6.1 What await does
  2. 6.2 What is saved across the pause
  3. 6.3 How the loop wakes the coroutine back up
  4. 6.4 A Task is a coroutine that the loop is driving
  5. 6.5 For the curious: coro.send(value) and the bytecode
  6. 6.6 For the curious: selectors and the operating system
  7. 6.7 Watching news.py yield and resume
  8. Summary

asyncio.run, create_task, gather

  1. 7.1 asyncio.run as the starting line
  2. 7.2 What asyncio.run does
  3. 7.3 await coro is sequential; create_task(coro) is concurrent
  4. 7.4 asyncio.gather for “run these together”
  5. 7.5 The fast news.py
  6. 7.6 Concurrent TCP probes
  7. 7.7 The lost-task trap
  8. Summary

The Time Toolbox

  1. 8.1 asyncio.sleep yields; time.sleep blocks
  2. 8.2 wait_for for a deadline around an awaitable
  3. 8.3 asyncio.timeout() as a context manager (Python 3.11+)
  4. 8.4 asyncio.shield to protect from cancellation
  5. 8.5 asyncio.wait vs asyncio.gather
  6. 8.6 asyncio.as_completed for results as they arrive
  7. 8.7 Timeouts and streaming
  8. Summary

async for, async with, Async Iteration and Resource Management

  1. 9.1 async for and the iterator protocol
  2. 9.2 Async generators
  3. 9.3 async for over aiohttp response bodies
  4. 9.4 async with and the context-manager protocol
  5. 9.5 Writing your own async context manager
  6. 9.6 The streaming news.py
  7. Summary

Tasks, TaskGroups, Cancellation, Exception Groups, and contextvars

  1. 10.1 Task is the loop’s unit of work; the coroutine is just the recipe
  2. 10.2 asyncio.TaskGroup: structured concurrency by construction
  3. 10.3 Cancellation: task.cancel() and CancelledError at the next await
  4. 10.4 The bare-except trap
  5. 10.5 ExceptionGroup and except*
  6. 10.6 Re-raising CancelledError after cleanup
  7. 10.7 Canceling a TCP probe
  8. 10.8 Synchronization primitives
  9. 10.9 Capping concurrency with Semaphore: 50 sites, 10 in flight
  10. 10.10 contextvars for task-scoped state
  11. 10.11 Producer-consumer with asyncio.Queue
  12. Summary

What Goes Wrong, and How to Find It

  1. 11.1 The failure mode: one synchronous call freezes the whole loop
  2. 11.2 The blocking patterns to learn to recognize
  3. 11.3 The single most useful tool: asyncio’s debug mode
  4. 11.4 The four warnings the runtime emits
  5. 11.5 asyncio.all_tasks() and task.get_stack()
  6. 11.6 aiomonitor: a live REPL inside a running program
  7. 11.7 Profiling that sees the loop correctly
  8. 11.8 Logging task names
  9. 11.9 pdb and async
  10. 11.10 A guided debugging session on the broken news.py
  11. 11.11 tcpcheck.py as a diagnostic tool
  12. Summary

Testing async code

  1. 12.1 Your first async test
  2. 12.2 The standard-library alternative
  3. 12.3 What mocks are, and why async needs a different one
  4. 12.4 Testing cancellation
  5. 12.5 Optional: Testing TaskGroup error propagation
  6. 12.6 Event loop fixture scope
  7. 12.7 Testing timeouts without real time
  8. 12.8 The most subtle async test bug
  9. 12.9 When a test hangs
  10. 12.10 The full test suite for the fetcher
  11. Summary

Where asyncio Ends: Executors, Graceful Shutdown

  1. 13.1 Fixing the parser bug from Chapter 11
  2. 13.2 The executor toolbox
  3. 13.3 The decision tree
  4. Heavy CPU work that needs to scale across cores Use multiprocessing directly. asyncio is the wrong tool for that workload.
  5. 13.4 Bridging from sync to async, and back
  6. 13.5 Library design: expose async, do not hide it
  7. 13.6 Graceful shutdown
  8. 13.7 The final news.py
  9. Summary

Appendix A: Setting up your environment

  1. A.1 Python versions and what they buy you
  2. A.2 The packages the book uses
  3. A.3 The smallest runnable asyncio script
  4. A.4 Reading the running example as you go

Appendix B: The asyncio API surface

  1. Functions
  2. asyncio.wait_for(coro, timeout) Wraps a single awaitable with a deadline; cancels the inner work and raises TimeoutError on expiry. 8
  3. Classes and protocols
  4. ExceptionGroup The exception type that holds multiple exceptions raised by sibling Tasks in a TaskGroup. Caught with except* (3.11+). 10
  5. Loop methods worth knowing
  6. loop.set_default_executor(executor) Replaces the loop’s default executor. Useful when the default thread pool is too small for the workload. 13
  7. aiohttp API surface used in the book
  8. response.text() Awaitable that returns the full response body as a decoded string. 5
  9. Where to find the rest

Appendix C: Troubleshooting guide

  1. C.1 Asyncio runtime warnings, what they mean and what to fix
  2. RuntimeWarning: async generator 'X' was never closed An async generator was garbage-collected mid-iteration without the consumer running it to completion or calling aclose(). Wrap resources held between yield in try/finally. Call aclose() explicitly when you need cleanup at a specific moment. (Chapter 9, Section 9.2.1.)
  3. C.2 The script runs without errors but is no faster than synchronous
  4. Slow-callback warnings fire (Executing took 0.234 seconds) when running with debug=True. A coroutine ran for too long between await, blocking the loop. Usually a CPU-bound function or a sync I/O call. Move the slow work to a thread pool with asyncio.to_thread. For CPU-bound work that needs cores, use a process pool via loop.run_in_executor(ProcessPoolExecutor(), ...). (Chapter 11; Chapter 13.)
  5. C.3 The program hangs or never exits
  6. Ctrl+C does not interrupt the program. No signal handler installed; or a signal handler that schedules async work but asyncio.run is blocked elsewhere. Install signal handlers with loop.add_signal_handler(sig, callback). Cancel the top-level Task in the callback. The TaskGroup propagates cancellation to children. (Chapter 13.)
  7. C.4 Common errors and what they mean
  8. RuntimeError: Event loop is closed (often during program shutdown) Code is trying to use the loop after asyncio.run returned and closed it. Common when a finalizer or __del__ method tries to schedule async work. Move any async cleanup into the running coroutine’s normal exit path (finally blocks, __aexit__). Do not rely on __del__ or atexit for async cleanup.
  9. When the table does not have your symptom

Appendix D: Further reading

  1. D.1 The asyncio source code
  2. D.2 The async library ecosystem
  3. D.3 The structured concurrency literature
  4. D.4 The PEPs that shaped asyncio
  5. D.5 Things this book did not cover, that you may want next

Glossary

About the Author

From the Author

Preface

What This Book Is About

What This Book Is Not

Who This Book Is For

How to Use This Book

Conventions Used in This Book

Companion Code and Data

How to Contact Us

Acknowledgments

Contents

The Blocking Function

  1. 1.1 The first version of news.py
  2. 1.2 What “blocking” actually means
  3. 1.3 The cost grows with the list
  4. 1.4 The same shape, one layer down
  5. 1.5 The question that drives the rest of the book
  6. Summary

Concurrency, Parallelism, and the GIL

  1. 2.1 What a thread is, briefly
  2. 2.2 The threaded version of news.py
  3. 2.3 Threading the TCP probes
  4. 2.4 Why the speedup actually works (and the GIL story you have heard wrong)
  5. 2.5 Why threads are not the answer the book is reaching for
  6. 2.6 The two scales the book cares about
  7. 2.7 Where the next chapter goes
  8. Summary

The Loop, From Scratch

  1. 3.1 A queue of jobs
  2. 3.2 A loop that runs the next ready job
  3. 3.3 Two senses of “ready”, and the generator that gives us pause
  4. 3.4 The loop sleeps when nothing is ready
  5. 3.5 Connecting the toy loop to news.py
  6. Summary

Coroutines, Without the Magic

  1. 4.1 Generators, the original coroutine
  2. 4.2 yield as a pause point, one more time
  3. 4.3 The toy loop
  4. 4.4 What async def returns
  5. Polite driver for x in gen(): (no for; the loop handles it)
  6. 4.5 Coroutines are not running until the loop runs them
  7. Summary

async and await

  1. 5.1 What async does to a function definition
  2. 5.2 What await does at a call site
  3. 5.3 What can be awaited, and what cannot
  4. 5.4 The first real asyncio news.py
  5. 5.5 The cliffhanger
  6. 5.6 The same keywords, on raw TCP
  7. Summary

await Yields, Suspends, and Resumes

  1. 6.1 What await does
  2. 6.2 What is saved across the pause
  3. 6.3 How the loop wakes the coroutine back up
  4. 6.4 A Task is a coroutine that the loop is driving
  5. 6.5 For the curious: coro.send(value) and the bytecode
  6. 6.6 For the curious: selectors and the operating system
  7. 6.7 Watching news.py yield and resume
  8. Summary

asyncio.run, create_task, gather

  1. 7.1 asyncio.run as the starting line
  2. 7.2 What asyncio.run does
  3. 7.3 await coro is sequential; create_task(coro) is concurrent
  4. 7.4 asyncio.gather for “run these together”
  5. 7.5 The fast news.py
  6. 7.6 Concurrent TCP probes
  7. 7.7 The lost-task trap
  8. Summary

The Time Toolbox

  1. 8.1 asyncio.sleep yields; time.sleep blocks
  2. 8.2 wait_for for a deadline around an awaitable
  3. 8.3 asyncio.timeout() as a context manager (Python 3.11+)
  4. 8.4 asyncio.shield to protect from cancellation
  5. 8.5 asyncio.wait vs asyncio.gather
  6. 8.6 asyncio.as_completed for results as they arrive
  7. 8.7 Timeouts and streaming
  8. Summary

async for, async with, Async Iteration and Resource Management

  1. 9.1 async for and the iterator protocol
  2. 9.2 Async generators
  3. 9.3 async for over aiohttp response bodies
  4. 9.4 async with and the context-manager protocol
  5. 9.5 Writing your own async context manager
  6. 9.6 The streaming news.py
  7. Summary

Tasks, TaskGroups, Cancellation, Exception Groups, and contextvars

  1. 10.1 Task is the loop’s unit of work; the coroutine is just the recipe
  2. 10.2 asyncio.TaskGroup: structured concurrency by construction
  3. 10.3 Cancellation: task.cancel() and CancelledError at the next await
  4. 10.4 The bare-except trap
  5. 10.5 ExceptionGroup and except*
  6. 10.6 Re-raising CancelledError after cleanup
  7. 10.7 Canceling a TCP probe
  8. 10.8 Synchronization primitives
  9. 10.9 Capping concurrency with Semaphore: 50 sites, 10 in flight
  10. 10.10 contextvars for task-scoped state
  11. 10.11 Producer-consumer with asyncio.Queue
  12. Summary

What Goes Wrong, and How to Find It

  1. 11.1 The failure mode: one synchronous call freezes the whole loop
  2. 11.2 The blocking patterns to learn to recognize
  3. 11.3 The single most useful tool: asyncio’s debug mode
  4. 11.4 The four warnings the runtime emits
  5. 11.5 asyncio.all_tasks() and task.get_stack()
  6. 11.6 aiomonitor: a live REPL inside a running program
  7. 11.7 Profiling that sees the loop correctly
  8. 11.8 Logging task names
  9. 11.9 pdb and async
  10. 11.10 A guided debugging session on the broken news.py
  11. 11.11 tcpcheck.py as a diagnostic tool
  12. Summary

Testing async code

  1. 12.1 Your first async test
  2. 12.2 The standard-library alternative
  3. 12.3 What mocks are, and why async needs a different one
  4. 12.4 Testing cancellation
  5. 12.5 Optional: Testing TaskGroup error propagation
  6. 12.6 Event loop fixture scope
  7. 12.7 Testing timeouts without real time
  8. 12.8 The most subtle async test bug
  9. 12.9 When a test hangs
  10. 12.10 The full test suite for the fetcher
  11. Summary

Where asyncio Ends: Executors, Graceful Shutdown

  1. 13.1 Fixing the parser bug from Chapter 11
  2. 13.2 The executor toolbox
  3. 13.3 The decision tree
  4. Heavy CPU work that needs to scale across cores Use multiprocessing directly. asyncio is the wrong tool for that workload.
  5. 13.4 Bridging from sync to async, and back
  6. 13.5 Library design: expose async, do not hide it
  7. 13.6 Graceful shutdown
  8. 13.7 The final news.py
  9. Summary

Appendix A: Setting up your environment

  1. A.1 Python versions and what they buy you
  2. A.2 The packages the book uses
  3. A.3 The smallest runnable asyncio script
  4. A.4 Reading the running example as you go

Appendix B: The asyncio API surface

  1. Functions
  2. asyncio.wait_for(coro, timeout) Wraps a single awaitable with a deadline; cancels the inner work and raises TimeoutError on expiry. 8
  3. Classes and protocols
  4. ExceptionGroup The exception type that holds multiple exceptions raised by sibling Tasks in a TaskGroup. Caught with except* (3.11+). 10
  5. Loop methods worth knowing
  6. loop.set_default_executor(executor) Replaces the loop’s default executor. Useful when the default thread pool is too small for the workload. 13
  7. aiohttp API surface used in the book
  8. response.text() Awaitable that returns the full response body as a decoded string. 5
  9. Where to find the rest

Appendix C: Troubleshooting guide

  1. C.1 Asyncio runtime warnings, what they mean and what to fix
  2. RuntimeWarning: async generator 'X' was never closed An async generator was garbage-collected mid-iteration without the consumer running it to completion or calling aclose(). Wrap resources held between yield in try/finally. Call aclose() explicitly when you need cleanup at a specific moment. (Chapter 9, Section 9.2.1.)
  3. C.2 The script runs without errors but is no faster than synchronous
  4. Slow-callback warnings fire (Executing took 0.234 seconds) when running with debug=True. A coroutine ran for too long between await, blocking the loop. Usually a CPU-bound function or a sync I/O call. Move the slow work to a thread pool with asyncio.to_thread. For CPU-bound work that needs cores, use a process pool via loop.run_in_executor(ProcessPoolExecutor(), ...). (Chapter 11; Chapter 13.)
  5. C.3 The program hangs or never exits
  6. Ctrl+C does not interrupt the program. No signal handler installed; or a signal handler that schedules async work but asyncio.run is blocked elsewhere. Install signal handlers with loop.add_signal_handler(sig, callback). Cancel the top-level Task in the callback. The TaskGroup propagates cancellation to children. (Chapter 13.)
  7. C.4 Common errors and what they mean
  8. RuntimeError: Event loop is closed (often during program shutdown) Code is trying to use the loop after asyncio.run returned and closed it. Common when a finalizer or __del__ method tries to schedule async work. Move any async cleanup into the running coroutine’s normal exit path (finally blocks, __aexit__). Do not rely on __del__ or atexit for async cleanup.
  9. When the table does not have your symptom

Appendix D: Further reading

  1. D.1 The asyncio source code
  2. D.2 The async library ecosystem
  3. D.3 The structured concurrency literature
  4. D.4 The PEPs that shaped asyncio
  5. D.5 Things this book did not cover, that you may want next

Glossary

About the Author

From the Author

Preface

What This Book Is About

What This Book Is Not

Who This Book Is For

How to Use This Book

Conventions Used in This Book

Companion Code and Data

How to Contact Us

Acknowledgments

Contents

The Blocking Function

  1. 1.1 The first version of news.py
  2. 1.2 What “blocking” actually means
  3. 1.3 The cost grows with the list
  4. 1.4 The same shape, one layer down
  5. 1.5 The question that drives the rest of the book
  6. Summary

Concurrency, Parallelism, and the GIL

  1. 2.1 What a thread is, briefly
  2. 2.2 The threaded version of news.py
  3. 2.3 Threading the TCP probes
  4. 2.4 Why the speedup actually works (and the GIL story you have heard wrong)
  5. 2.5 Why threads are not the answer the book is reaching for
  6. 2.6 The two scales the book cares about
  7. 2.7 Where the next chapter goes
  8. Summary

The Loop, From Scratch

  1. 3.1 A queue of jobs
  2. 3.2 A loop that runs the next ready job
  3. 3.3 Two senses of “ready”, and the generator that gives us pause
  4. 3.4 The loop sleeps when nothing is ready
  5. 3.5 Connecting the toy loop to news.py
  6. Summary

Coroutines, Without the Magic

  1. 4.1 Generators, the original coroutine
  2. 4.2 yield as a pause point, one more time
  3. 4.3 The toy loop
  4. 4.4 What async def returns
  5. Polite driver for x in gen(): (no for; the loop handles it)
  6. 4.5 Coroutines are not running until the loop runs them
  7. Summary

async and await

  1. 5.1 What async does to a function definition
  2. 5.2 What await does at a call site
  3. 5.3 What can be awaited, and what cannot
  4. 5.4 The first real asyncio news.py
  5. 5.5 The cliffhanger
  6. 5.6 The same keywords, on raw TCP
  7. Summary

await Yields, Suspends, and Resumes

  1. 6.1 What await does
  2. 6.2 What is saved across the pause
  3. 6.3 How the loop wakes the coroutine back up
  4. 6.4 A Task is a coroutine that the loop is driving
  5. 6.5 For the curious: coro.send(value) and the bytecode
  6. 6.6 For the curious: selectors and the operating system
  7. 6.7 Watching news.py yield and resume
  8. Summary

asyncio.run, create_task, gather

  1. 7.1 asyncio.run as the starting line
  2. 7.2 What asyncio.run does
  3. 7.3 await coro is sequential; create_task(coro) is concurrent
  4. 7.4 asyncio.gather for “run these together”
  5. 7.5 The fast news.py
  6. 7.6 Concurrent TCP probes
  7. 7.7 The lost-task trap
  8. Summary

The Time Toolbox

  1. 8.1 asyncio.sleep yields; time.sleep blocks
  2. 8.2 wait_for for a deadline around an awaitable
  3. 8.3 asyncio.timeout() as a context manager (Python 3.11+)
  4. 8.4 asyncio.shield to protect from cancellation
  5. 8.5 asyncio.wait vs asyncio.gather
  6. 8.6 asyncio.as_completed for results as they arrive
  7. 8.7 Timeouts and streaming
  8. Summary

async for, async with, Async Iteration and Resource Management

  1. 9.1 async for and the iterator protocol
  2. 9.2 Async generators
  3. 9.3 async for over aiohttp response bodies
  4. 9.4 async with and the context-manager protocol
  5. 9.5 Writing your own async context manager
  6. 9.6 The streaming news.py
  7. Summary

Tasks, TaskGroups, Cancellation, Exception Groups, and contextvars

  1. 10.1 Task is the loop’s unit of work; the coroutine is just the recipe
  2. 10.2 asyncio.TaskGroup: structured concurrency by construction
  3. 10.3 Cancellation: task.cancel() and CancelledError at the next await
  4. 10.4 The bare-except trap
  5. 10.5 ExceptionGroup and except*
  6. 10.6 Re-raising CancelledError after cleanup
  7. 10.7 Canceling a TCP probe
  8. 10.8 Synchronization primitives
  9. 10.9 Capping concurrency with Semaphore: 50 sites, 10 in flight
  10. 10.10 contextvars for task-scoped state
  11. 10.11 Producer-consumer with asyncio.Queue
  12. Summary

What Goes Wrong, and How to Find It

  1. 11.1 The failure mode: one synchronous call freezes the whole loop
  2. 11.2 The blocking patterns to learn to recognize
  3. 11.3 The single most useful tool: asyncio’s debug mode
  4. 11.4 The four warnings the runtime emits
  5. 11.5 asyncio.all_tasks() and task.get_stack()
  6. 11.6 aiomonitor: a live REPL inside a running program
  7. 11.7 Profiling that sees the loop correctly
  8. 11.8 Logging task names
  9. 11.9 pdb and async
  10. 11.10 A guided debugging session on the broken news.py
  11. 11.11 tcpcheck.py as a diagnostic tool
  12. Summary

Testing async code

  1. 12.1 Your first async test
  2. 12.2 The standard-library alternative
  3. 12.3 What mocks are, and why async needs a different one
  4. 12.4 Testing cancellation
  5. 12.5 Optional: Testing TaskGroup error propagation
  6. 12.6 Event loop fixture scope
  7. 12.7 Testing timeouts without real time
  8. 12.8 The most subtle async test bug
  9. 12.9 When a test hangs
  10. 12.10 The full test suite for the fetcher
  11. Summary

Where asyncio Ends: Executors, Graceful Shutdown

  1. 13.1 Fixing the parser bug from Chapter 11
  2. 13.2 The executor toolbox
  3. 13.3 The decision tree
  4. Heavy CPU work that needs to scale across cores Use multiprocessing directly. asyncio is the wrong tool for that workload.
  5. 13.4 Bridging from sync to async, and back
  6. 13.5 Library design: expose async, do not hide it
  7. 13.6 Graceful shutdown
  8. 13.7 The final news.py
  9. Summary

Appendix A: Setting up your environment

  1. A.1 Python versions and what they buy you
  2. A.2 The packages the book uses
  3. A.3 The smallest runnable asyncio script
  4. A.4 Reading the running example as you go

Appendix B: The asyncio API surface

  1. Functions
  2. asyncio.wait_for(coro, timeout) Wraps a single awaitable with a deadline; cancels the inner work and raises TimeoutError on expiry. 8
  3. Classes and protocols
  4. ExceptionGroup The exception type that holds multiple exceptions raised by sibling Tasks in a TaskGroup. Caught with except* (3.11+). 10
  5. Loop methods worth knowing
  6. loop.set_default_executor(executor) Replaces the loop’s default executor. Useful when the default thread pool is too small for the workload. 13
  7. aiohttp API surface used in the book
  8. response.text() Awaitable that returns the full response body as a decoded string. 5
  9. Where to find the rest

Appendix C: Troubleshooting guide

  1. C.1 Asyncio runtime warnings, what they mean and what to fix
  2. RuntimeWarning: async generator 'X' was never closed An async generator was garbage-collected mid-iteration without the consumer running it to completion or calling aclose(). Wrap resources held between yield in try/finally. Call aclose() explicitly when you need cleanup at a specific moment. (Chapter 9, Section 9.2.1.)
  3. C.2 The script runs without errors but is no faster than synchronous
  4. Slow-callback warnings fire (Executing took 0.234 seconds) when running with debug=True. A coroutine ran for too long between await, blocking the loop. Usually a CPU-bound function or a sync I/O call. Move the slow work to a thread pool with asyncio.to_thread. For CPU-bound work that needs cores, use a process pool via loop.run_in_executor(ProcessPoolExecutor(), ...). (Chapter 11; Chapter 13.)
  5. C.3 The program hangs or never exits
  6. Ctrl+C does not interrupt the program. No signal handler installed; or a signal handler that schedules async work but asyncio.run is blocked elsewhere. Install signal handlers with loop.add_signal_handler(sig, callback). Cancel the top-level Task in the callback. The TaskGroup propagates cancellation to children. (Chapter 13.)
  7. C.4 Common errors and what they mean
  8. RuntimeError: Event loop is closed (often during program shutdown) Code is trying to use the loop after asyncio.run returned and closed it. Common when a finalizer or __del__ method tries to schedule async work. Move any async cleanup into the running coroutine’s normal exit path (finally blocks, __aexit__). Do not rely on __del__ or atexit for async cleanup.
  9. When the table does not have your symptom

Appendix D: Further reading

  1. D.1 The asyncio source code
  2. D.2 The async library ecosystem
  3. D.3 The structured concurrency literature
  4. D.4 The PEPs that shaped asyncio
  5. D.5 Things this book did not cover, that you may want next

Glossary

About the Author

From the Author

Preface

What This Book Is About

What This Book Is Not

Who This Book Is For

How to Use This Book

Conventions Used in This Book

Companion Code and Data

How to Contact Us

Acknowledgments

Contents

The Blocking Function

  1. 1.1 The first version of news.py
  2. 1.2 What “blocking” actually means
  3. 1.3 The cost grows with the list
  4. 1.4 The same shape, one layer down
  5. 1.5 The question that drives the rest of the book
  6. Summary

Concurrency, Parallelism, and the GIL

  1. 2.1 What a thread is, briefly
  2. 2.2 The threaded version of news.py
  3. 2.3 Threading the TCP probes
  4. 2.4 Why the speedup actually works (and the GIL story you have heard wrong)
  5. 2.5 Why threads are not the answer the book is reaching for
  6. 2.6 The two scales the book cares about
  7. 2.7 Where the next chapter goes
  8. Summary

The Loop, From Scratch

  1. 3.1 A queue of jobs
  2. 3.2 A loop that runs the next ready job
  3. 3.3 Two senses of “ready”, and the generator that gives us pause
  4. 3.4 The loop sleeps when nothing is ready
  5. 3.5 Connecting the toy loop to news.py
  6. Summary

Coroutines, Without the Magic

  1. 4.1 Generators, the original coroutine
  2. 4.2 yield as a pause point, one more time
  3. 4.3 The toy loop
  4. 4.4 What async def returns
  5. Polite driver for x in gen(): (no for; the loop handles it)
  6. 4.5 Coroutines are not running until the loop runs them
  7. Summary

async and await

  1. 5.1 What async does to a function definition
  2. 5.2 What await does at a call site
  3. 5.3 What can be awaited, and what cannot
  4. 5.4 The first real asyncio news.py
  5. 5.5 The cliffhanger
  6. 5.6 The same keywords, on raw TCP
  7. Summary

await Yields, Suspends, and Resumes

  1. 6.1 What await does
  2. 6.2 What is saved across the pause
  3. 6.3 How the loop wakes the coroutine back up
  4. 6.4 A Task is a coroutine that the loop is driving
  5. 6.5 For the curious: coro.send(value) and the bytecode
  6. 6.6 For the curious: selectors and the operating system
  7. 6.7 Watching news.py yield and resume
  8. Summary

asyncio.run, create_task, gather

  1. 7.1 asyncio.run as the starting line
  2. 7.2 What asyncio.run does
  3. 7.3 await coro is sequential; create_task(coro) is concurrent
  4. 7.4 asyncio.gather for “run these together”
  5. 7.5 The fast news.py
  6. 7.6 Concurrent TCP probes
  7. 7.7 The lost-task trap
  8. Summary

The Time Toolbox

  1. 8.1 asyncio.sleep yields; time.sleep blocks
  2. 8.2 wait_for for a deadline around an awaitable
  3. 8.3 asyncio.timeout() as a context manager (Python 3.11+)
  4. 8.4 asyncio.shield to protect from cancellation
  5. 8.5 asyncio.wait vs asyncio.gather
  6. 8.6 asyncio.as_completed for results as they arrive
  7. 8.7 Timeouts and streaming
  8. Summary

async for, async with, Async Iteration and Resource Management

  1. 9.1 async for and the iterator protocol
  2. 9.2 Async generators
  3. 9.3 async for over aiohttp response bodies
  4. 9.4 async with and the context-manager protocol
  5. 9.5 Writing your own async context manager
  6. 9.6 The streaming news.py
  7. Summary

Tasks, TaskGroups, Cancellation, Exception Groups, and contextvars

  1. 10.1 Task is the loop’s unit of work; the coroutine is just the recipe
  2. 10.2 asyncio.TaskGroup: structured concurrency by construction
  3. 10.3 Cancellation: task.cancel() and CancelledError at the next await
  4. 10.4 The bare-except trap
  5. 10.5 ExceptionGroup and except*
  6. 10.6 Re-raising CancelledError after cleanup
  7. 10.7 Canceling a TCP probe
  8. 10.8 Synchronization primitives
  9. 10.9 Capping concurrency with Semaphore: 50 sites, 10 in flight
  10. 10.10 contextvars for task-scoped state
  11. 10.11 Producer-consumer with asyncio.Queue
  12. Summary

What Goes Wrong, and How to Find It

  1. 11.1 The failure mode: one synchronous call freezes the whole loop
  2. 11.2 The blocking patterns to learn to recognize
  3. 11.3 The single most useful tool: asyncio’s debug mode
  4. 11.4 The four warnings the runtime emits
  5. 11.5 asyncio.all_tasks() and task.get_stack()
  6. 11.6 aiomonitor: a live REPL inside a running program
  7. 11.7 Profiling that sees the loop correctly
  8. 11.8 Logging task names
  9. 11.9 pdb and async
  10. 11.10 A guided debugging session on the broken news.py
  11. 11.11 tcpcheck.py as a diagnostic tool
  12. Summary

Testing async code

  1. 12.1 Your first async test
  2. 12.2 The standard-library alternative
  3. 12.3 What mocks are, and why async needs a different one
  4. 12.4 Testing cancellation
  5. 12.5 Optional: Testing TaskGroup error propagation
  6. 12.6 Event loop fixture scope
  7. 12.7 Testing timeouts without real time
  8. 12.8 The most subtle async test bug
  9. 12.9 When a test hangs
  10. 12.10 The full test suite for the fetcher
  11. Summary

Where asyncio Ends: Executors, Graceful Shutdown

  1. 13.1 Fixing the parser bug from Chapter 11
  2. 13.2 The executor toolbox
  3. 13.3 The decision tree
  4. Heavy CPU work that needs to scale across cores Use multiprocessing directly. asyncio is the wrong tool for that workload.
  5. 13.4 Bridging from sync to async, and back
  6. 13.5 Library design: expose async, do not hide it
  7. 13.6 Graceful shutdown
  8. 13.7 The final news.py
  9. Summary

Appendix A: Setting up your environment

  1. A.1 Python versions and what they buy you
  2. A.2 The packages the book uses
  3. A.3 The smallest runnable asyncio script
  4. A.4 Reading the running example as you go

Appendix B: The asyncio API surface

  1. Functions
  2. asyncio.wait_for(coro, timeout) Wraps a single awaitable with a deadline; cancels the inner work and raises TimeoutError on expiry. 8
  3. Classes and protocols
  4. ExceptionGroup The exception type that holds multiple exceptions raised by sibling Tasks in a TaskGroup. Caught with except* (3.11+). 10
  5. Loop methods worth knowing
  6. loop.set_default_executor(executor) Replaces the loop’s default executor. Useful when the default thread pool is too small for the workload. 13
  7. aiohttp API surface used in the book
  8. response.text() Awaitable that returns the full response body as a decoded string. 5
  9. Where to find the rest

Appendix C: Troubleshooting guide

  1. C.1 Asyncio runtime warnings, what they mean and what to fix
  2. RuntimeWarning: async generator 'X' was never closed An async generator was garbage-collected mid-iteration without the consumer running it to completion or calling aclose(). Wrap resources held between yield in try/finally. Call aclose() explicitly when you need cleanup at a specific moment. (Chapter 9, Section 9.2.1.)
  3. C.2 The script runs without errors but is no faster than synchronous
  4. Slow-callback warnings fire (Executing took 0.234 seconds) when running with debug=True. A coroutine ran for too long between await, blocking the loop. Usually a CPU-bound function or a sync I/O call. Move the slow work to a thread pool with asyncio.to_thread. For CPU-bound work that needs cores, use a process pool via loop.run_in_executor(ProcessPoolExecutor(), ...). (Chapter 11; Chapter 13.)
  5. C.3 The program hangs or never exits
  6. Ctrl+C does not interrupt the program. No signal handler installed; or a signal handler that schedules async work but asyncio.run is blocked elsewhere. Install signal handlers with loop.add_signal_handler(sig, callback). Cancel the top-level Task in the callback. The TaskGroup propagates cancellation to children. (Chapter 13.)
  7. C.4 Common errors and what they mean
  8. RuntimeError: Event loop is closed (often during program shutdown) Code is trying to use the loop after asyncio.run returned and closed it. Common when a finalizer or __del__ method tries to schedule async work. Move any async cleanup into the running coroutine’s normal exit path (finally blocks, __aexit__). Do not rely on __del__ or atexit for async cleanup.
  9. When the table does not have your symptom

Appendix D: Further reading

  1. D.1 The asyncio source code
  2. D.2 The async library ecosystem
  3. D.3 The structured concurrency literature
  4. D.4 The PEPs that shaped asyncio
  5. D.5 Things this book did not cover, that you may want next

Glossary

About the Author

Get the free sample chapters

Click the buttons to get the free sample in PDF or EPUB, or read the sample online here

The Leanpub 60 Day 100% Happiness Guarantee

Within 60 days of purchase you can get a 100% refund on any Leanpub purchase, in two clicks.

Now, this is technically risky for us, since you'll have the book or course files either way. But we're so confident in our products and services, and in our authors and readers, that we're happy to offer a full money back guarantee for everything we sell.

You can only find out how good something is by trying it, and because of our 100% money back guarantee there's literally no risk to do so!

So, there's no reason not to click the Add to Cart button, is there?

See full terms...

Earn $8 on a $10 Purchase, and $16 on a $20 Purchase

We pay 80% royalties on purchases of $7.99 or more, and 80% royalties minus a 50 cent flat fee on purchases between $0.99 and $7.98. You earn $8 on a $10 sale, and $16 on a $20 sale. So, if we sell 5000 non-refunded copies of your book for $20, you'll earn $80,000.

(Yes, some authors have already earned much more than that on Leanpub.)

In fact, authors have earned over $15 million writing, publishing and selling on Leanpub.

Learn more about writing on Leanpub

Free Updates. DRM Free.

If you buy a Leanpub book, you get free updates for as long as the author updates the book! Many authors use Leanpub to publish their books in-progress, while they are writing them. All readers get free updates, regardless of when they bought the book or how much they paid (including free).

Most Leanpub books are available in PDF (for computers) and EPUB (for phones, tablets and Kindle). The formats that a book includes are shown at the top right corner of this page.

Finally, Leanpub books don't have any DRM copy-protection nonsense, so you can easily read them on any supported device.

Learn more about Leanpub's ebook formats and where to read them

Write and Publish on Leanpub

You can use Leanpub to easily write, publish and sell in-progress and completed ebooks and online courses!

Leanpub is a powerful platform for serious authors, combining a simple, elegant writing and publishing workflow with a store focused on selling in-progress ebooks.

Leanpub is a magical typewriter for authors: just write in plain text, and to publish your ebook, just click a button. (Or, if you are producing your ebook your own way, you can even upload your own PDF and/or EPUB files and then publish with one click!) It really is that easy.

Learn more about writing on Leanpub