From 0fa30c0fc3149848c7d52e9f063e22aba757b8b9 Mon Sep 17 00:00:00 2001 From: nursen-akay73 Date: Sat, 6 Jun 2026 15:05:08 +0300 Subject: [PATCH] feat: add question about async/await and the event loop Add interview question #16 covering Python's native async/await syntax: concurrency vs. parallelism, event loop mechanics, coroutines/awaitables/ tasks, structured concurrency with asyncio.TaskGroup (3.11+), async context managers/iterators, and common pitfalls. All examples use Python 3.12+ syntax. Co-authored-by: Cursor --- README.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/README.md b/README.md index f417be3..7f3e43e 100644 --- a/README.md +++ b/README.md @@ -935,6 +935,105 @@ if __name__ == "__main__": This ensures that the block following `if __name__ == "__main__":` only runs when the script is executed directly, keeping the module "clean" for external imports.
+## 16. What are _async_/_await_ in _Python_, and how does the _event loop_ work? + +`async`/`await` is Python's native syntax (introduced in **PEP 492**, Python 3.5) for writing **coroutine-based concurrency**. It enables a single thread to handle thousands of **I/O-bound** operations cooperatively, without the overhead of OS threads or the contention of the **Global Interpreter Lock (GIL)**. + +### Concurrency vs. Parallelism + +A crucial distinction that interviewers probe: + +- **Concurrency**: Dealing with many tasks by **interleaving** their execution (one CPU core, tasks take turns). This is what `asyncio` provides. +- **Parallelism**: Executing many tasks **simultaneously** on multiple cores (e.g., `multiprocessing`). + +Because of the **GIL**, `asyncio` does *not* speed up **CPU-bound** work — it shines for **I/O-bound** workloads (network calls, disk, databases) where the program would otherwise sit idle waiting on `read()`/`write()` syscalls. + +### How the Event Loop Works + +The **event loop** is the orchestrator. It maintains a queue of ready-to-run tasks and a set of resources being awaited (sockets, timers). + +1. A coroutine runs until it hits an `await` on something not yet ready (e.g., a network response). +2. At that point it **yields control** back to the loop, returning a state machine that "remembers" where it paused. +3. The loop registers the awaited resource (often via `selectors`/`epoll`/`kqueue`) and runs **other** ready tasks. +4. When the OS signals the resource is ready, the loop **resumes** the suspended coroutine exactly where it left off. + +This cooperative model means a single blocking call (e.g., `time.sleep`, a heavy CPU loop) **freezes the entire loop**, starving every other task. + +### Coroutines, Awaitables, and Tasks + +- **Coroutine**: The object returned by calling an `async def` function. Calling it does **nothing** until it is awaited or scheduled. +- **Awaitable**: Anything usable with `await` — coroutines, `Task`s, and `Future`s. +- **Task**: A coroutine *wrapped and scheduled* on the loop so it runs concurrently with others. + +```python +import asyncio + +async def fetch(name: str, delay: float) -> str: + await asyncio.sleep(delay) # non-blocking; yields to the loop + return f"{name} done in {delay}s" + +async def main() -> None: + # Sequential: ~3s total (each awaited one after another) + print(await fetch("A", 1)) + print(await fetch("B", 2)) + +asyncio.run(main()) # asyncio.run sets up and tears down the loop +``` + +### Running Coroutines Concurrently + +The real value appears when tasks overlap. A common interview trap is awaiting calls sequentially when they could be concurrent. + +```python +import asyncio + +async def fetch(name: str, delay: float) -> str: + await asyncio.sleep(delay) + return name + +# Modern, structured approach (Python 3.11+): TaskGroup +async def main() -> None: + async with asyncio.TaskGroup() as tg: + t1 = tg.create_task(fetch("A", 1)) + t2 = tg.create_task(fetch("B", 2)) + # Block exits only when all tasks finish (~2s, not 3s) + print(t1.result(), t2.result()) + +asyncio.run(main()) +``` + +`asyncio.TaskGroup` is preferred over the older `asyncio.gather` because it provides **structured concurrency**: if one child task raises, the others are **cancelled**, and exceptions propagate together via an `ExceptionGroup` (catchable with `except*`). + +### Async Context Managers and Iterators + +Modern async code frequently uses `async with` and `async for`, backed by `__aenter__`/`__aexit__` and `__anext__`. + +```python +import asyncio +from collections.abc import AsyncIterator + +async def stream_lines() -> AsyncIterator[int]: + for i in range(3): + await asyncio.sleep(0.1) + yield i # this is an async generator + +async def main() -> None: + async for value in stream_lines(): + print(value) + +asyncio.run(main()) +``` + +### Common Pitfalls + +- **Forgetting `await`**: `fetch(...)` alone creates a coroutine that never runs and emits a `RuntimeWarning: coroutine was never awaited`. +- **Blocking the loop**: Calling synchronous blocking code (e.g., `requests.get`, `time.sleep`) stalls everything. Offload it with `await asyncio.to_thread(blocking_fn, ...)`. +- **Fire-and-forget tasks**: A `Task` referenced by nothing can be garbage-collected mid-flight; keep a reference or use a `TaskGroup`. +- **Mixing sync and async**: You cannot `await` inside a regular `def`, and you cannot call `asyncio.run()` from within an already-running loop. + +In professional 2026 development, `async`/`await` underpins high-throughput web frameworks (FastAPI, ASGI servers like Uvicorn) and is the standard for scalable network services where threads would be too costly. +
+ #### Explore all 100 answers here 👉 [Devinterview.io - Python](https://devinterview.io/questions/web-and-mobile-development/python-interview-questions)