Build1 distinct publisher3 min readPublished
contextlib.suppress discards the exception exactly as try/except/pass does, but it is a call expression rather than a handler, so rules that dispatch on handlers never see it. One new analyzer counts the gap.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
The dispatch path is the whole story. S110 and B110 are written against `ExceptHandler` nodes: walk the tree, find a handler whose body is a bare `pass`, emit a diagnostic [1]. After the rewrite there is no handler to find. `contextlib.suppress(FileNotFoundError)` is a `with` item wrapping a call, and according to the author of failroute every exception-handling rule in the shipped linters dispatches on handlers [5]. Nothing is broken. The rule is asking a question about a node that the fix deleted [4].
SIM105 is good style advice on its own terms. The `with` form is shorter and it does not tempt you into widening the except clause while you are in there. The side effect is jurisdictional: autofixing a repository full of S110 hits moves every one of those sites from a shape two shipped rules can read into a shape none of them can [3][6]. A fix that relocates the finding out of the finder's reach is an unusual kind of fix.
The benchmark needs reading carefully. failroute reports 613 findings across the source packages of eight AI and eval repositories, against 67 for ruff's exception rules [7][8]. Most of that gap is rule inventory rather than detection skill. Of the 613, 77 are suppress blocks and 403 are silent-fallback or masked-exception handlers, leaving 133 in the other categories [15]. Ruff has no rule that expresses "this handler returns 0.0 and the caller cannot tell", so the comparison is partly between a tool built for that defect family and tools that never claimed it. The narrower figure is the one that carries: 77 suppress blocks is already more than the 67 findings ruff's exception rules produce over the same code [16].
For the ratio to transfer, your code has to look like theirs. Async clients, model calls, retries wrapped around flaky network I/O, and a habit of returning a neutral value when a dependency is down. A service with one global error middleware will surface far fewer sites; a numerics library may surface many that are all deliberate. The author is explicit that each finding is a routing decision a reviewer should have made on purpose, and that sometimes discarding the failure is correct, which is what the opt-out marker exists for [9].
Precision and recall are pinned at 1.0 against failroute's own hand-labelled corpus [10]. That is a claim about agreement with the person who wrote the labels, which is the strongest thing you can gate in CI and weaker than field precision. Two details make it load-bearing anyway. The suppress fixtures were committed before the detector existed, and the benchmark ran red on exactly the five expected misses [11]. Then the corpus caught the first draft flagging `contextlib.suppress(asyncio.CancelledError)`, which handler rules already exempt in its `except` form; v0.5.1 unified the ignore lists across both syntaxes [12].
If I were adopting this, I would turn on the suppress rule first. It is one AST pattern, the triage is bounded by 77-ish sites in code of that shape [7], and the answer per site is a yes or no a reviewer can give in a minute. The 403-strong fallback category can wait for a repository where somebody owns the error contract.
Ranked by verification strength, evidence, and original report placement.
Ruff's S110 rule flags try-except-pass and Bandit's B110 does the same, with a message that is some variant of "consider logging the exception".
No shipped linter flags contextlib.suppress with valid arguments: not ruff, not Bandit, not bugbear, not pylint. The only related rule is bugbear's B022, which fires on contextlib.suppress() with no arguments.
Ruff's SIM105 rule exists specifically to recommend "Use contextlib.suppress(...) instead of try-except-pass", and a project that runs autofix on SIM105 converts every flagged handler into a suppress block.
contextlib.suppress(FileNotFoundError) around os.remove(path) is semantically identical to try/except FileNotFoundError: pass; the failure is discarded either way.
The author states that contextlib.suppress(...) with valid arguments is a call expression rather than an ExceptHandler, and that every exception-handling rule in the shipped linters dispatches on handlers.
The author says the detection gap is real and shipped as of publication, and in SIM105's case is actively widened by autofix.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 27, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
Ruff Does Not Detect Duplicate Code, And That Gap Now Has Its Own Tool1 distinct publisher
build
Pydantic AI re-sends the whole run history every step, so token spend goes quadratic by design1 distinct publisher
build
Arid 2.0 keeps the detector and rebuilds the plumbing around it1 distinct publisher
build
The one Pylint check Ruff cannot take, and the case for pulling it out of CI1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Verifiable mechanism, self-issued numbers
This story splits cleanly down the middle. The mechanism — S110 and B110 fire on handlers, suppress with arguments is a call expression, SIM105 rewrites one into the other — is checkable in an afternoon by anyone with ruff and bandit installed, and the author volunteers the structural reason instead of asking to be believed. Everything quantitative sits on the other side of the line: the 613 findings, the 67-finding comparison, the five-miss red run and the precision claim are all outputs of the author's own analyzer against a corpus he labelled, and no second party has touched them.
Shipped, but only its own CI runs it
failroute is real and installable — PyPI, SARIF, a pre-commit hook, a GitHub Action — and the only codebase confirmed to run it is its own, where the expected alert count is zero. The eight repositories in the benchmark were scanned, not adopters; the post ends by asking eval and red-team maintainers whether the tool finds anything in their code, which is where a project stands before adoption rather than after it. Against that, the pattern it hunts is described as ubiquitous, so the gap here is between the problem's reach and the tool's.
Headline outruns the count
"Actively migrating silent failure into an undetectable idiom" is a claim about the ecosystem's direction; what supports it is a rule that recommends a rewrite and 77 suppress blocks found in eight repositories by one tool. Nothing here shows how often SIM105 autofix is switched on, and nothing shows that the 77 blocks arrived via autofix rather than by developers writing modern Python. The author is unusually careful where it counts — he narrows the claim himself, exempts CancelledError, and concedes that discarding a failure is sometimes correct — which keeps the overstatement to the framing rather than the substance.
The gap-finder sells the gap-filler
The person reporting that no shipped linter sees this idiom is the person shipping the linter that does, and the piece closes with install instructions and a request for user reports. That alignment does not make the blind spot imaginary — it is structural and checkable — but it shapes which numbers got produced and published: a comparison in which the author's tool returns 613 and Ruff returns 67 was chosen by the author. Worth noting on the other side that he discloses the tool is his throughout and documents his own false positive.
Sound argument, unaudited arithmetic
We can be fairly firm about what a linter does and does not see, and much less firm about what that costs anyone. A single self-published post, a single author, no upstream comment from Ruff, Bandit, bugbear or pylint, and no third party who has run the scan. Our reading would move quickly on two things: another party reproducing the counts, or any of the eight scanned projects saying whether the findings were real.