<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: fei</title>
    <description>The latest articles on DEV Community by fei (@feiiiiii5).</description>
    <link>https://dev.to/feiiiiii5</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4098386%2F9dbdda6b-8f75-423f-bb13-08df7dc0b93c.jpg</url>
      <title>DEV Community: fei</title>
      <link>https://dev.to/feiiiiii5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/feiiiiii5"/>
    <language>en</language>
    <item>
      <title>The linter ecosystem is actively migrating silent failure into an undetectable idiom</title>
      <dc:creator>fei</dc:creator>
      <pubDate>Fri, 28 Aug 2026 06:17:57 +0000</pubDate>
      <link>https://dev.to/feiiiiii5/the-linter-ecosystem-is-actively-migrating-silent-failure-into-an-undetectable-idiom-2h70</link>
      <guid>https://dev.to/feiiiiii5/the-linter-ecosystem-is-actively-migrating-silent-failure-into-an-undetectable-idiom-2h70</guid>
      <description>&lt;p&gt;In a well-known family of LLM evaluation bugs, the failure doesn't look like a failure. A judge model goes down, and the benchmark keeps publishing scores — because somewhere in the code, a handler like this turned an outage into a legitimate-looking zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;llm_judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hit this pattern repeatedly while fixing correctness bugs across mainstream AI/ML open source. So I did what you do when the same defect family keeps appearing: I built a detector for it — &lt;a href="https://github.com/feiiiiii5/failroute" rel="noopener noreferrer"&gt;failroute&lt;/a&gt;, a static analyzer that flags failures converted into success-looking values. It's on PyPI, it runs in CI, and every number in this post is reproducible from the repository.&lt;/p&gt;

&lt;p&gt;But the interesting finding of the last week is not my tool. It's about the ecosystem it lives in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idiom that learned to hide
&lt;/h2&gt;

&lt;p&gt;Python has two ways to write "ignore failures here". The old one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every linter has an opinion about that shape. Ruff's &lt;code&gt;S110&lt;/code&gt; flags try-except-pass. Bandit's &lt;code&gt;B110&lt;/code&gt; does the same. The message is always some variant of "consider logging the exception".&lt;/p&gt;

&lt;p&gt;The modern one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suppress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Semantically identical — the failure is discarded either way. And here is the part that surprised me: &lt;strong&gt;no shipped linter flags it&lt;/strong&gt;. Not ruff, not Bandit, not bugbear, not pylint. The only related rule is bugbear's &lt;code&gt;B022&lt;/code&gt;, which fires on &lt;code&gt;contextlib.suppress()&lt;/code&gt; with &lt;em&gt;no arguments&lt;/em&gt; — the case where nothing is even suppressed.&lt;/p&gt;

&lt;p&gt;It gets better. Ruff's &lt;code&gt;SIM105&lt;/code&gt; rule exists specifically to recommend this rewrite: "Use &lt;code&gt;contextlib.suppress(...)&lt;/code&gt; instead of &lt;code&gt;try&lt;/code&gt;-&lt;code&gt;except&lt;/code&gt;-&lt;code&gt;pass&lt;/code&gt;." A project that runs autofix on &lt;code&gt;SIM105&lt;/code&gt; converts every flagged handler into a suppress block — and moves all of them out of every existing detector's view in one automated sweep. The silence is unchanged. The syntax learned to hide.&lt;/p&gt;

&lt;p&gt;I want to be careful about the claim here, because "linter X doesn't catch Y" is easy to assert and hard to defend. This one is defensible: &lt;code&gt;contextlib.suppress(...)&lt;/code&gt; with valid arguments is a call expression, not an &lt;code&gt;ExceptHandler&lt;/code&gt;, and every exception-handling rule in the shipped linters dispatches on handlers. As of today, the detection gap is real, shipped, and — in &lt;code&gt;SIM105&lt;/code&gt;'s case — actively widened by autofix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it matter in real code?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;contextlib.suppress&lt;/code&gt; is everywhere in modern Python. In the source packages of eight real AI/eval repositories (garak, inspect_ai, pydantic-ai, uqlm, trl, smolagents, deepteam, fickling), I counted &lt;strong&gt;77 suppress blocks&lt;/strong&gt;, sitting alongside &lt;strong&gt;403 silent-fallback / masked-exception handlers&lt;/strong&gt; that syntactic rules also cannot express — 613 findings total, against &lt;strong&gt;67&lt;/strong&gt; for ruff's exception rules. Each finding in the benchmark is a routing decision a reviewer should have made explicitly: &lt;em&gt;is discarding this failure correct here?&lt;/em&gt; Sometimes yes — that's what the opt-out marker is for. Often it's the judge-outage pattern wearing a cleaner syntax.&lt;/p&gt;

&lt;p&gt;None of this means suppress is bad. &lt;code&gt;contextlib.suppress(CancelledError)&lt;/code&gt; is idiomatic cancellation absorption and failroute doesn't flag it, for exactly the reason handler rules exempt &lt;code&gt;except asyncio.CancelledError&lt;/code&gt;. The point is narrower: &lt;strong&gt;a routing decision that used to be visible for review is migrating into a form where review tooling cannot see it, and the migration is automated.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a detector for this needs (and what I learned building one)
&lt;/h2&gt;

&lt;p&gt;Syntactic detection of suppress is trivial — it's one AST pattern. The hard part is the same as for any semantic defect family: &lt;strong&gt;precision&lt;/strong&gt;, because a linter that cries wolf gets disabled.&lt;/p&gt;

&lt;p&gt;failroute's answer is a hand-labelled corpus. Every rule must match fixtures whose ground truth was written from the &lt;em&gt;semantics&lt;/em&gt; of each case, independently of tool output, with precision and recall pinned at 1.0. The corpus has already paid for itself twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The suppress fixtures were added &lt;em&gt;before&lt;/em&gt; the detector existed. The benchmark ran red — exactly the five expected misses — which is what makes "labels independent of tool output" a testable claim rather than a slogan.&lt;/li&gt;
&lt;li&gt;The corpus then caught a design mistake before release: the first draft flagged &lt;code&gt;contextlib.suppress(asyncio.CancelledError)&lt;/code&gt;. But absorbing cancellation is idiomatic control flow, and the handler rules already exempt &lt;code&gt;except asyncio.CancelledError&lt;/code&gt;. Same semantics, two syntaxes, two answers — a bug. v0.5.1 unified the ignore lists across both syntaxes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The broader lesson for anyone building tooling with AI assistance: the AI is good at proposing detector logic and finding the gap; the deterministic gate (a corpus that must first fail, then pass) is what turns a plausible-sounding rule into a defensible one. The full loop, including the failure cases, is documented in the repository's &lt;a href="https://github.com/feiiiiii5/failroute/blob/main/docs/process.md" rel="noopener noreferrer"&gt;process doc&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;failroute
failroute &lt;span class="nt"&gt;--repo&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;        &lt;span class="c"&gt;# text/JSON/SARIF, exit code CI-friendly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a pre-commit hook, a GitHub Action that uploads findings to code scanning, and a self-scan of the repository itself in CI (expected alerts: zero). If you maintain an eval or red-team framework, I'd genuinely like to know whether it finds anything real in your codebase — and if it finds something that isn't real, I want to hear about that too, because that's the corpus growing.&lt;/p&gt;




</description>
      <category>python</category>
      <category>linter</category>
      <category>testing</category>
      <category>staticanalysis</category>
    </item>
  </channel>
</rss>
