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.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

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

The event loop is a scheduler that repeatedly:

It's the "orchestra conductor" of asynchronous execution.

A coroutine is a function that can be paused:

Coroutines don't run until awaited or turned into a Task.

A Task wraps a coroutine and schedules it on the event loop so it runs concurrently.

This is how we achieve concurrency in a single thread.

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.

Tasks are built on Futures — every Task is a subclass of Future.

A task runs until it hits an await that yields control:

Timeouts are essential for robust production systems.

This gives event-loop-level control that frameworks use internally.

This is how modern backend services fetch data from multiple microservices at once.

This pattern lets you scrape hundreds of pages per second.

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

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.