We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Concurrency Without Regret step 9 of 38
map_with_failures: partial success on a thread pool
Fan work out to a thread pool without letting one bad item take the batch with it.
def map_with_failures[T, R](
fn: Callable[[T], R], items: Sequence[T], *, max_workers: int
) -> tuple[list[R], list[tuple[int, BaseException]]]:
- Successes come back in input order.
-
Failures come back as
(index, exception)pairs, ordered by index. - Every item is attempted; one raising must never stop the others.
- Every exception is observed exactly once.
- The pool is shut down on every exit path.
Note BaseException, not Exception. A worker that raises KeyboardInterrupt
still has that exception stored on its future, and a map_with_failures that
only catches Exception will drop it on the floor.
Why Executor.map cannot do this
map() raises the worker’s exception when you reach that element of the
result iterator, and cancels the not-yet-started futures at that point. You get
the first few results, an exception, and no record of which of the remaining
items ran. Partial success is not expressible. submit plus a
future-to-index mapping is.
The other trap map carries: it consumes the whole input iterable immediately,
so a lazy generator is fully materialised before a single result appears.
Python 3.14 added buffersize= for exactly that; on 3.12 there is no fix
except chunking yourself.
What the report proves
-
values— input order. The driver’sfnsleeps longest for the earliest item, so completion order is the reverse of input order. Appending as futures complete fails here. -
failures/failure_order— every failure captured once, keyed by input index, returned in index order. -
attempted— equals the item count in every case, including the ones where everything fails. A short-circuit is visible here. -
thread_delta— worker threads alive before minus after. Must be0. A pool that is never shut down leaves its idle workers parked on the queue forever, and this is what catches it.
Where the type system earns its keep
Future[R] is generic, and submit is precisely typed: submit(fn, item)
checks item against fn‘s parameter and gives you a Future[R] whose
.result() is an R. Compare Thread(target=fn, args=(item,)) where args
is effectively untyped — nothing checks the tuple against the target’s
signature, and there is no result type at all because there is nowhere for a
result to go.
Under --strict the future-to-index dict wants a real annotation
(dict[Future[R], int]), and that annotation is what makes the two return
lists provably homogeneous instead of list[Any].
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.