Representative interview topic

Coding interview: When should Python asyncio use an eager task factory?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

An asyncio service calls many cached coroutines. Should it enable asyncio.eager_task_factory? Explain execution order, exceptions, cancellation, TaskGroup, version compatibility, rollout, and rollback.

Prompt and context

An async service often satisfies requests from an in-memory cache without doing I/O, yet every coroutine still becomes a task and waits for event-loop scheduling. The team proposes setting asyncio.eager_task_factory globally. Evaluate the trade-off and propose a safe rollout.

Python documents that eager execution starts a coroutine synchronously while its Task is constructed; the Task is scheduled only when the coroutine blocks. The API was added in Python 3.12 and changes observable scheduling semantics.

What the interviewer is testing

The key is distinguishing synchronous completion from asynchronous blocking and explaining effects on fairness, ordering, exception timing, cancellation, and TaskGroup. Strong answers restrict the optimization to measured boundaries and include version checks, feature flags, rollback, and event-loop metrics.

Clarifying questions to ask first

  • What is the minimum Python version across every deployment?
  • Are these coroutines memory-cache reads, or can they perform network, database, file, or lock I/O?
  • Does code depend on task-creation order, loop fairness, or call_soon ordering?
  • Are failure, cancellation, and deadlines owned by a TaskGroup or request scope?
  • Is the target task-creation CPU, tail latency, or throughput, and what is the baseline?

30-second answer framework

“I would not enable it globally first. An eager factory starts a coroutine during Task construction, so a cache hit can avoid one event-loop scheduling turn; a blocking coroutine is scheduled normally. That changes ordering, exception timing, and fairness: a loop creating many synchronous tasks can delay timers and other requests. I would check the Python version, roll out at measured cache call sites, compare event-loop lag and tail latency, and keep the default factory as an immediate rollback.”

Step-by-step deep dive

Step 1: State the default and eager models

With the default factory, create_task schedules the coroutine to run soon. With an eager factory, construction immediately advances it until it returns, raises, or reaches its first blocking await. A synchronous completion may never enter the event-loop queue.

python
loop.set_task_factory(asyncio.eager_task_factory)
task = asyncio.create_task(read_cached(key))

Therefore eager execution is an observable semantic change, not merely a faster scheduler.

Step 2: Choose the coroutine boundary

Good candidates are short, high-hit-rate memory-cache or memoized operations. Coroutines that may perform network, database, file, lock, or unbounded CPU work should keep a predictable blocking boundary so construction cannot monopolize the current task.

Step 3: Analyze order and fairness

When tasks are created in a loop, eager coroutines can complete immediately in creation order, changing an interleaving that previously depended on the event loop. A large synchronous batch can delay timers, I/O callbacks, and other requests. Track event-loop lag and bound batch size.

Step 4: Handle exceptions and cancellation

An exception raised before the first block can surface near create_task, changing the capture point and stack shape. Keep Task references, and let CancelledError propagate after cleanup. Never use eager mode to turn request cancellation or a cache failure into a successful response.

Step 5: Combine it with TaskGroup and deadlines

TaskGroup still owns the task tree, sibling cancellation, and join, but a child may already have completed or failed during create_task. Use structured try/except* handling and one outer deadline; do not assume every child first waits in a queue.

python
async with asyncio.TaskGroup() as group:
    user = group.create_task(read_cached("user"))
    orders = group.create_task(read_remote("orders"))

Step 6: Check versions and scope the feature

The factory is available from Python 3.12. Python 3.14 also exposes an eager_start option on create_task. A multi-version service should validate the runtime at startup and prefer a local call-site experiment over an unconditional global factory change.

Step 7: Roll out with rollback metrics

Start at cache-only call sites behind a switch. Compare CPU, task-construction time, cache-hit tail latency, event-loop lag, exception rate, cancellation rate, and downstream QPS against the default factory. If fairness or errors regress, restore the default without changing business code.

Step 8: Test semantics, not only benchmarks

Cover synchronous return, synchronous raise, first await, external cancellation, multiple TaskGroup failures, deadlines, recursive task creation, timer fairness, and mixed cache/remote batches. Record events and assert ordering; a throughput benchmark alone cannot validate scheduling semantics.

Model high-quality answer

“Eager execution fits short, predictable cache coroutines because it removes a scheduling turn; it is risky for unpredictable I/O or long CPU paths. The main risk is changed ordering, fairness, and exception timing. I would check versions, enable it locally behind a flag, retain the default rollback, and monitor event-loop lag, tail latency, cancellation, and errors. TaskGroup and a shared deadline remain in place, as do normal cancellation and cleanup.”

Common mistakes

  • Treating eager mode as semantics-free → ordering and exception timing change → write down both execution models and measure.
  • Enabling it for every coroutine → slow I/O or CPU blocks the current task → canary only short synchronous paths.
  • Ignoring Python versions → older deployments fail at startup → validate the runtime and retain the default factory.
  • Looking only at average throughput → fairness silently regresses → add loop-lag and tail metrics.
  • Assuming TaskGroup is unchanged → construction-time failures are mishandled → test eager child failures and exception groups.
  • Swallowing cancellation or cleanup errors → requests leak work → preserve cancellation and verify release paths.

Follow-up questions and strong responses

Follow-up 1: Does a synchronous cache hit still leave a Task?

The Task may already be complete during construction. Do not require an event-loop turn; read the returned object and test the eager-completion path explicitly.

Follow-up 2: Does eager mode guarantee creation order for completion?

No. It changes start timing only; once a coroutine blocks, normal scheduling applies. If business order matters, coordinate explicitly or sort results by an operation key.

Follow-up 3: How do you prevent a cache-hit batch from starving requests?

Bound the batch, yield between batches when appropriate, and watch loop lag. Applying eager mode to one low-cost call site is safer than a global policy.

Follow-up 4: How do you roll it back after a regression?

Disable the switch, restore the default task factory, verify metrics return to baseline, and retain traces containing runtime version and event order for diagnosis.

Follow-up 5: How does eager_start relate to the factory?

eager_start is an explicit option for one Task; the factory is an event-loop default policy. Confirm the exact Python-version signature and precedence before combining them.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool