We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Failure by Design step 9 of 18
Failure aggregation over batches
A validator that reports every field failure at once is the clearest non-async motivation for exception groups. Nobody enjoys the API that rejects a twelve-field form one field per round trip — and nobody enjoys the batch job that dies on record 3 of 40 000 and tells you nothing about records 4 through 40 000.
The shape you want: attempt every record, collect the failures, and decide
at the end whether the batch as a whole failed. Along the way, tag each
collected exception with the record that produced it — because by the time that
exception surfaces three layers up, the loop variable that knew the id is long
gone. add_note() is the tool for exactly that, and it travels with the
exception through the group and out to the traceback.
What to build
def process_batch(records: Sequence[Mapping[str, str]], handler: Handler) -> BatchReport:
For each record, in order:
-
call
handler(record); -
on
Exception, callexc.add_note(f"record={record_id}"), collect it, and record the id as failed; - on success, record the id as succeeded;
-
a
BaseExceptionthat is not anExceptionmust propagate immediately and must not be collected. The handler that raisesKeyboardInterruptis hostile on purpose: a batch runner that keeps grinding through 40 000 records after the operator hit Ctrl-C is the failure mode this rule exists to prevent.
Afterwards: if there were failures and nothing succeeded, raise
ExceptionGroup("every record in the batch failed", errors). Otherwise return
BatchReport(succeeded, failed, errors) with the three tuples in encounter
order.
Watch the empty batch. all([]) is True, so the naive spelling of “every
record failed” raises on a batch of zero records — and ExceptionGroup with an
empty member list raises ValueError anyway. Phrase the condition so that an
empty batch produces an empty report, which is the only sane answer.
The probe
def solve(records: Sequence[Mapping[str, str]]) -> dict[str, object]:
Each record is {"id": <str>, "action": <str>} where the action is one of:
| action | handler behaviour |
|---|---|
"ok" |
returns |
"transient" |
raises TransientError(f"{record_id} timed out") |
"terminal" |
raises TerminalError(f"{record_id} is malformed") |
"abort" |
raises KeyboardInterrupt(f"{record_id} interrupted the worker") |
The handler appends each record id to a shared attempted list before doing
anything else.
Now consume the result with except*, which is the point of the exercise:
try:
try:
report = process_batch(records, handler)
except* TransientError as transient_group:
...
except* TerminalError as terminal_group:
...
except BaseException as exc:
...
Two things that shape this code and are not negotiable. You cannot mix
except and except* on the same try, which is why the BaseException
boundary is an outer statement. And return is a syntax error inside an
except* clause — so each clause assigns to a variable and the returns happen
afterwards. Both restrictions come from the same place: an except* block may
run more than once conceptually, so control flow out of it is ill-defined.
Return exactly one of three shapes:
{"outcome": "report", "succeeded": <tuple>, "failed": <tuple>, "errors": [...], "attempted": [...]}
{"outcome": "group", "transient": [...], "terminal": [...], "attempted": [...]}
{"outcome": "aborted", "abort_type": <class name>, "attempted": [...]}
Every exception is rendered as {"type": <class name>, "message": str(exc), "notes": [...]}. For "group", flatten each matched subgroup down to its leaf
members — a subgroup is a tree, not a flat list.
succeeded and failed are the BatchReport tuples, returned as tuples.
The grader distinguishes a tuple from a list, deliberately: an immutable report
is an immutable report.
What the type checker buys you
type Handler = Callable[[Mapping[str, str]], None] (PEP 695, 3.12) names the
seam once. except* TransientError as transient_group binds an
ExceptionGroup[TransientError] — not a TransientError — so the helper that
flattens it must accept a group, and mypy will tell you if you forgot. That is
the single most common first-contact mistake with except*.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.