We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Failure by Design step 6 of 18
Bare except, blind except Exception, and where to catch
try/except Exception: pass around a database write is data loss that nobody
discovers for a month. A bare except: in a worker loop means Ctrl-C does
nothing and your deploy hangs until someone reaches for kill -9.
Both come from the same misunderstanding, so start there. A bare except:
catches BaseException, which is strictly larger than Exception. The
extra members are the ones you must never swallow:
-
KeyboardInterrupt— the operator asked you to stop. -
SystemExit—sys.exit()is in flight; swallowing it turns a clean shutdown into an infinite loop. -
GeneratorExit— a generator is being closed; swallowing it is aRuntimeErrorwaiting to happen. -
and, in async code,
asyncio.CancelledError, which since 3.8 is aBaseExceptionspecifically so thatexcept Exceptioncannot eat it.
PEP 760, “No More Bare Excepts”, was withdrawn. Bare except: is legal
Python forever, so enforcement is a linter’s job: ruff’s E722 for the bare
form and BLE001 for blind except Exception.
Where to catch, not just what
The rule that survives review:
- Narrowly, near the raise, when you can actually recover — you know which exception, and you know what to do about it.
-
Broadly, only at a boundary — a request handler, a worker loop, a
main(). There, one blanketexcept Exceptionthat logs withexc_info=Trueand converts the failure into an error response is not sloppy, it is the correct and necessary thing. A boundary that lets one bad request kill the process is worse than a boundary that catches too much.
run_all below is a boundary. It is supposed to be greedy — and greedy stops
exactly at the Exception/BaseException line.
What to build
The starter contains a run_all that is subtly, plausibly wrong. Fix it.
def run_all(tasks: Sequence[Callable[[], None]]) -> list[tuple[int, Exception]]:
Run every task in order. Collect (index, exception) for each one that raises
an Exception, and keep going — one bad task must not stop the batch. Anything
that is a BaseException but not an Exception propagates immediately, and
the failures collected so far are discarded along with it.
You do not need an isinstance check to express that. Say what you mean and
the interpreter does the rest.
The probe
def solve(tasks: Sequence[Mapping[str, str]]) -> dict[str, object]:
Each spec is {} for a task that succeeds, or {"raises": <name>, "message": <text>} for one that raises RAISABLE[name](message). Build one callable per
spec. Every task must append its own index to a shared list before it
raises, so the result records what actually ran.
Return exactly these keys:
| key | value |
|---|---|
"outcome" |
"completed" if run_all returned, "aborted" if a BaseException escaped it |
"abort_type" |
the escaping exception’s class name, else None |
"started" |
the list of indices that began executing, in order |
"failures" |
for "completed": one {"index", "type", "message"} dict per collected failure, in order. For "aborted": [] |
Note what "started" proves. After a task at index 1 raises ValueError, the
task at index 2 must still appear. After a task at index 1 raises
KeyboardInterrupt, it must not.
Typing notes
list[tuple[int, Exception]] is doing real work: append a BaseException to it
and mypy rejects the line. The starter’s bug is therefore a type error as
well as a behavioural one — which is the whole argument for the gate. Fixing
the annotation to BaseException to silence mypy makes the tests fail instead,
and that is the honest signal.
str(KeyError("head")) is "'head'", quotes included. That is KeyError
being KeyError, not a bug in your code.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.