Asyncio
AsyncIO is the backbone of asynchronous programming in Python. To build high-performance systems — APIs, websocket servers, scrapers, automation pipelines, or distributed workers — you must fully understand how the event loop works, how Tasks provide concurrency, and how Futures act as low-level building blocks.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • How the asyncio event loop schedules and switches between coroutines
- • The difference between Tasks and Futures — and when to use each
- • How to run multiple async operations concurrently with asyncio.gather
- • How to create and cancel Tasks, and handle timeouts safely
- • How to use asyncio.Queue for producer-consumer pipelines
- • Common async patterns used in real APIs, scrapers, and websocket servers
🔥 1. What Exactly Is the Event Loop?
The event loop is a scheduler that repeatedly:
- Picks an awaitable that is ready to run
- Executes a small portion of it
- Pauses it when it awaits I/O
- Switches to the next ready task
- Handles callbacks, timers, and I/O events
It's the "orchestra conductor" of asynchronous execution.
- Create event loop
- Run coroutine
- Clean up loop
⚙️ 2. Creating Coroutines (The Basics)
A coroutine is a function that can be paused:
Coroutines don't run until awaited or turned into a Task.
🧠 3. Tasks — The Core of Concurrency
A Task wraps a coroutine and schedules it on the event loop so it runs concurrently.
- create_task() schedules coroutine immediately
- Execution overlaps with the rest of the program
- Awaiting task retrieves result
This is how we achieve concurrency in a single thread.
⚡ 4. Running Multiple Tasks Concurrently
- Fetching from many APIs
- Processing many files
- Running many workers
- Web scraping
- Database batch loading
🌀 5. Futures — Low-Level Awaitables
A Future represents a placeholder for a value that isn't available yet.
You rarely create Futures manually, but Tasks and event-loop internals rely on them.
- Futures hold results that arrive later
- Callbacks can resolve Futures
Tasks are built on Futures — every Task is a subclass of Future.
⏳ 6. Understanding How Tasks Progress
A task runs until it hits an await that yields control:
- Step 1 runs → hits sleep → yields
- Step 2 runs → hits sleep → yields
- Event loop resumes Step 1 and Step 2
🧩 7. Task Cancellation
- Shutting down servers
- Stopping background loops gracefully
🧱 8. Task Groups (Python 3.11+)
- Automatic error propagation
- Structured concurrency
- Cleaner code than gather()
⚡ 9. Wait vs Gather — When To Use Which?
- Returns results
- Cancels all tasks if one fails
- Best for symmetric jobs
- More control
- Choose FIRST_COMPLETED, FIRST_EXCEPTION
- Best for: Race conditions
- Redundant API fetches
- Timeout logic
⏱️ 10. Using Timeouts Correctly
Timeouts are essential for robust production systems.
🔄 11. Callbacks & Event Loop Scheduling
This gives event-loop-level control that frameworks use internally.
🛰️ 12. Real-World Example — Concurrent API Fetching
This is how modern backend services fetch data from multiple microservices at once.
📡 13. Real-World Example — WebScraping With Concurrency
This pattern lets you scrape hundreds of pages per second.
🔥 14. Production Architecture Using Tasks
- Session cleanup
- Cache warmers
- Message queue consumers
- API request-response cycles
- Graceful shutdown
- Task cancellation
- Producer/consumer pipelines
🎉 Conclusion
You've mastered three critical components of AsyncIO:
Concurrent execution wrappers built on Futures
Low-level placeholders controlling async flow
Together, these form the foundation of every major async Python framework (FastAPI, Starlette, aiohttp).
📋 Quick Reference — AsyncIO
Syntax
What it does
asyncio.get_event_loop()
Get the current event loop
asyncio.create_task(coro)
Schedule coroutine as background task
asyncio.wait_for(coro, timeout)
Add timeout to a coroutine
asyncio.Queue()
Thread-safe async queue
async for / async with
Async iteration and context managers
You now know how the asyncio event loop works internally, how to manage Tasks, and how to build async pipelines.
Up next: Concurrency — compare threads vs processes and choose the right model.
Practice quiz
What does asyncio.run(main()) do?
- Defines a coroutine without running it
- Schedules main() as a background task
- Creates an event loop, runs the coroutine, then closes the loop
- Runs main() in a separate process
Answer: Creates an event loop, runs the coroutine, then closes the loop. asyncio.run() creates an event loop, runs the top-level coroutine, and cleans up the loop.
When does a plain coroutine actually start executing?
- Only when awaited or turned into a Task
- As soon as it is defined
- When the module is imported
- Immediately on the next line
Answer: Only when awaited or turned into a Task. Coroutines don't run until they are awaited or scheduled as a Task.
What is the relationship between Tasks and Futures in asyncio?
- They are unrelated
- Every Future is a subclass of Task
- Futures replaced Tasks in Python 3.11
- Every Task is a subclass of Future
Answer: Every Task is a subclass of Future. Tasks are built on Futures — every Task is a subclass of Future.
What does asyncio.create_task(coro()) do?
- Awaits the coroutine and blocks
- Wraps the coroutine in a Task and schedules it to run concurrently
- Creates a new event loop
- Runs the coroutine in a thread
Answer: Wraps the coroutine in a Task and schedules it to run concurrently. create_task wraps a coroutine in a Task and schedules it on the running event loop.
Running two coroutines that each await asyncio.sleep(1) with asyncio.gather takes about how long?
- 1 second
- 2 seconds
- 0 seconds
- It depends on CPU cores
Answer: 1 second. gather overlaps the awaits, so total runtime is ~1 second, not 2.
What does a Future represent?
- A finished computation
- A new OS thread
- A placeholder for a value that isn't available yet
- A synchronous callback
Answer: A placeholder for a value that isn't available yet. A Future is a placeholder for a result that will arrive later.
Compared with gather, what extra control does asyncio.wait give you?
- It runs tasks in parallel processes
- You can choose return_when=FIRST_COMPLETED or FIRST_EXCEPTION
- It automatically retries failed tasks
- It guarantees ordered results
Answer: You can choose return_when=FIRST_COMPLETED or FIRST_EXCEPTION. wait returns (done, pending) and lets you specify FIRST_COMPLETED, ALL_COMPLETED, or FIRST_EXCEPTION.
How do you add a timeout to an awaitable?
- asyncio.timeout_after(coro, 3)
- coro.timeout(3)
- asyncio.sleep(3, coro)
- asyncio.wait_for(coro, timeout=3)
Answer: asyncio.wait_for(coro, timeout=3). asyncio.wait_for(coro, timeout=3) raises asyncio.TimeoutError if the coroutine takes too long.
What happens to a task when you call task.cancel()?
- It is paused and can resume later
- asyncio.CancelledError is raised inside the task
- It returns None immediately
- The whole event loop stops
Answer: asyncio.CancelledError is raised inside the task. cancel() schedules a CancelledError to be raised inside the task, which it can catch to clean up.
What is a key benefit of asyncio.TaskGroup (Python 3.11+) over gather?
- It runs on multiple cores
- It is faster for CPU-bound work
- Automatic error propagation and structured concurrency
- It avoids the event loop entirely
Answer: Automatic error propagation and structured concurrency. TaskGroup provides structured concurrency with automatic error propagation and cleaner code than gather.