Loading...
Loading...
Python interview guide
Threads, processes, the GIL and async code. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
31 questionsLast updated
A coroutine function is defined with async def. Calling it returns a coroutine object instead of running the body. The body runs when the object is awaited or scheduled by the event loop, and it can pause at each await and be resumed later. A normal function runs to completion once called.
The event loop is the scheduler at the centre of asyncio. It runs on one thread, keeps track of which coroutines are ready and which are waiting for I/O or timers, runs a ready one until it awaits, then picks the next. When a wait completes, it makes that coroutine ready again.
Lesson: async, await and the Event LoopThreads are switched by the operating system at any moment, so shared data needs locks. Asyncio tasks run on one thread and switch only at await, which is cooperative and predictable, and tasks are far cheaper than threads. The cost is that every library in the path must be async-aware, and one blocking call stops everything.
It holds the only thread, so the event loop cannot run any other task until the call returns. Timers fire late, other requests stall, and the service looks frozen. Use an async version of the call, or run it with asyncio.to_thread or an executor.
No. One event loop uses one thread, so it uses one core. It makes waiting efficient, not calculation. For CPU-bound work, run processes next to it, for example with loop.run_in_executor and a ProcessPoolExecutor.
You called an async function but did not await it or schedule it, so the body never ran. The fix is to add await, or wrap it in asyncio.create_task or gather.
For CPU-heavy work, for code that depends on blocking libraries with no async version, and for small scripts with a few waits where plain threads are simpler. It pays off with many simultaneous waits, such as thousands of network connections.
Lesson: async, await and the Event Loopawait coro and asyncio.create_task(coro)?await coro runs the coroutine and waits for it right there, so the next line starts after it ends. create_task schedules the coroutine to run concurrently and returns a Task straight away, so you can start several and await them later. Without tasks, awaiting calls in a row is sequential.
asyncio.gather order its results?It returns a list in the same order as the arguments, whatever order the tasks finish. That makes results easy to line up with inputs. If you want results in finishing order, use asyncio.as_completed or asyncio.wait.
gather?By default the first exception is raised to the awaiting code, but the other tasks are not cancelled and keep running. With return_exceptions=True, exceptions are returned as items in the list, so you can handle each one. A TaskGroup is safer, since it cancels the rest and raises an ExceptionGroup.
To cap concurrency: for example, at most 20 requests to a server at once. Each task enters async with sem: so excess tasks wait. It protects the remote service from overload and your own process from too many open sockets.
For new code on Python 3.11 or later I prefer TaskGroup. It waits for all its tasks at the end of the block, cancels siblings when one fails, and cannot leak a task. Gather is shorter when I want results in order and use return_exceptions=True, and it works on older Python.
gather parallel?No. It is concurrent: many tasks make progress by waiting at the same time on one thread. Nothing runs Python code in parallel. For parallel calculation you need processes.
Lesson: Tasks, gather and TaskGroupThe event loop keeps only a weak reference to tasks. If nothing else refers to a task, the garbage collector can delete it before it finishes, and its work stops silently. Store tasks in a set or list, or use a TaskGroup.
Lesson: Tasks, gather and TaskGroupUse await asyncio.wait_for(coro, timeout), which cancels the work and raises TimeoutError when time runs out. On Python 3.11 and later, async with asyncio.timeout(seconds): puts one deadline over a block of awaits. Always catch TimeoutError and decide whether to retry, fall back or fail.
task.cancel() schedules a CancelledError to be raised inside the task at its next await. The task can run finally blocks to clean up and then ends as cancelled. Because it needs an await, a task in a tight loop with no await cannot be cancelled until it awaits.
CancelledError?Timeouts, TaskGroups and shutdown all work by cancelling tasks. If a coroutine catches CancelledError and carries on, the cancel does not reach the top, and the program may hang or ignore timeouts. Clean up in finally, or catch it, clean up and re-raise.
wait_for when it times out?wait_for cancels the inner coroutine, waits for the cancellation to finish, and then raises TimeoutError. If the inner work ignores cancellation, the wait can last longer than the timeout. The work is not silently left running.
Create a task per server, call asyncio.wait(tasks, return_when=FIRST_COMPLETED), read the result from the done set, and cancel everything pending. This cuts latency and stops wasted work. It is sometimes called a hedged request.
Loop a small number of attempts. Wrap each attempt in wait_for so it has its own timeout, catch only the errors worth retrying, and sleep between attempts with a growing delay. Stop after the limit and raise a clear error. Retrying forever can overload the service you depend on.
HTTP calls spend nearly all their time waiting for the network. With an async client, the event loop can run thousands of requests on one thread, since a waiting request costs a small task instead of a thread. For a few calls, threads are just as good; async pays off at high concurrency.
Lesson: Async HTTP Calls and Blocking Coderequests and httpx or aiohttp in async code?requests is blocking: calling it inside a coroutine freezes the event loop. httpx.AsyncClient and aiohttp have coroutine methods that await the network, so other tasks run during the wait. If you must use a blocking library, call it through asyncio.to_thread.
Cap concurrency with an asyncio.Semaphore, set a timeout on each request, retry a few times with a growing pause, and respect the server's rate limits. A shared client with a connection limit helps too. Spawning a task per URL with no limit can open thousands of connections at once.
Catch errors inside each task and return a value such as a status or an error marker, or use gather(..., return_exceptions=True). Then the good results are kept, and you can retry or report the failed ones. With plain gather, one error is raised and the rest is lost to the caller.
Run it in a worker thread with await asyncio.to_thread(func, *args), or loop.run_in_executor with an executor. For CPU-heavy functions use a ProcessPoolExecutor so the GIL does not hold the loop back. This keeps the event loop free to run other tasks.
An async context manager implements __aenter__ and __aexit__, used with async with; both steps may await, so it can open and close connections. An async generator is an async def with yield, read with async for; it can await between items, for example when fetching pages one after another.
I ask what the program waits on. CPU-bound work needs processes, since each gets its own GIL and core. I/O-bound work with a moderate number of waits suits threads, which need almost no change to normal code. With thousands of concurrent waits, asyncio is lightest, but all the libraries in the path must be async or run in threads.
Lesson: AsyncIO CheckpointUse asyncio with one shared async HTTP client. Cap concurrency with a Semaphore, add a timeout per request, retry a few times with a growing pause, and return each result as a value so failures do not lose the rest. Collect with gather, keep input order, and respect the site's rate limits.
Calling a blocking function inside a coroutine; forgetting await so the coroutine never runs; creating tasks and not keeping references; swallowing CancelledError; no timeouts; unlimited concurrency; and running CPU-heavy code on the loop. Each one either stalls the loop or hides errors.
Avoid depending on the order tasks finish. Use gather, which keeps order, or sort the results. Replace real network calls with fake coroutines that use asyncio.sleep with different delays. Check outcomes rather than timings, for example a timeout fires, not how many milliseconds it took.
Because one event loop runs everything on one thread, and switching happens only at an await. A calculation holds the thread until it ends, so other tasks wait. Move it to a process pool with run_in_executor, and await the future.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson