A practice prompt we wrote. No company or candidate report names it, so it carries no company tag.

How to answer

The prompt names three properties, and each has a way to fake it. Say the invariant for each before you write code, then write the tests that prove them.

  1. Ask what the tasks do. Waiting on a network means asyncio or threads; CPU work means processes, and the pool’s shape stays the same. Ask whether the input is a list or a stream, whether results must keep input order, and whether one failure should stop the rest.
  2. Cap with a fixed set of workers, not a semaphore over every task. N workers pull from one shared iterator. Say the bound out loud: at most N tasks exist at once, and nothing reads the input ahead of the workers. A semaphore around a coroutine for every input caps what runs but still creates them all.
  3. One outcome per input, recorded the moment it is taken. Each outcome holds the index, the input and either a value or the exception. The invariant is that no input is taken without an outcome, so a failure can’t vanish. Catch Exception, never BaseException, so cancellation still works.
  4. A timeout per task. A hung call is a failure of that task, not a stall of the pool.
  5. Two kinds of shutdown. Graceful: stop taking new work, let what is running finish, report where to resume. Hard: cancel, and prove that no task outlives the pool.
  6. Test the properties, not the timing. Count peak concurrency, match every input to an outcome, cancel a pool of hung tasks.

The trap is asyncio.gather(*tasks) without return_exceptions=True. The first error propagates, the other tasks keep running with nobody watching, and every other result and error is lost.

Follow-ups

What the interviewer may ask next, once your first answer is on the table.

  • The input is a generator of URLs too large to fit in memory. What does your pool hold at once?
  • The customer’s API allows a fixed number of requests per second, not a fixed number at once. What changes, and where?
  • A task fails with HTTP 429. Does the pool retry it, or the task, and who decides the backoff?
  • The tasks are CPU-bound image resizes. What do you swap out, and what stays the same?
  • The caller wants each result written as soon as it finishes, not in one list at the end. What does the function return now?

Where answers go wrong

  • Creates a coroutine or future for every input up front and caps only how many run, so memory grows with the input even though parallelism is capped.
  • Uses asyncio.gather without return_exceptions, so the first error propagates, the other tasks keep running unobserved, and their results and errors are lost.
  • Catches BaseException, or swallows CancelledError, so the pool can no longer be shut down.

Answer this in two minutes

Write the answer you would say out loud. The clock starts with your first word.

Two minutes

Model answer

“Before I write it: the tasks call a customer API, so they’re I/O-bound and I’ll use asyncio on Python 3.11 or later. The input may be a generator, so I never materialize it and I read it only when a worker is free.