<?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: Chirag Patankar</title>
    <description>The latest articles on DEV Community by Chirag Patankar (@chirag_patankar).</description>
    <link>https://dev.to/chirag_patankar</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%2F4194832%2Fb26fa1dd-eaa0-4c51-969f-ee3392f8b5f7.png</url>
      <title>DEV Community: Chirag Patankar</title>
      <link>https://dev.to/chirag_patankar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chirag_patankar"/>
    <language>en</language>
    <item>
      <title>I scanned a month of CI from pydantic, aiohttp, fastapi and pandas for flaky tests. Here's what I found.</title>
      <dc:creator>Chirag Patankar</dc:creator>
      <pubDate>Mon, 19 Oct 2026 20:15:39 +0000</pubDate>
      <link>https://dev.to/chirag_patankar/i-scanned-a-month-of-ci-from-pydantic-aiohttp-fastapi-and-pandas-for-flaky-tests-heres-what-i-3lk1</link>
      <guid>https://dev.to/chirag_patankar/i-scanned-a-month-of-ci-from-pydantic-aiohttp-fastapi-and-pandas-for-flaky-tests-heres-what-i-3lk1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vxtxd0w8zmcbpthuimx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vxtxd0w8zmcbpthuimx.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every project with CI has a test that fails for no reason. Someone clicks &lt;strong&gt;Re-run&lt;/strong&gt;, it goes green, and everyone moves on. Nobody writes it down, so the same test wastes someone else's afternoon next week.&lt;/p&gt;

&lt;p&gt;I wanted to know how much of this is going on in well-run open-source projects, so I built a tool to measure it, &lt;a href="https://github.com/ChiragPatankar/Flake-doctor" rel="noopener noreferrer"&gt;flake-doctor&lt;/a&gt;, and pointed it at 30 days of GitHub Actions history from four popular Python projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: same commit, different result
&lt;/h2&gt;

&lt;p&gt;You can't tell a flaky test from a real bug by looking at one failure. But CI history contains a much stronger signal: &lt;strong&gt;a job that failed and then passed on the same commit&lt;/strong&gt;. Same code, different outcome. Whatever failed in between is flaky by definition.&lt;/p&gt;

&lt;p&gt;That happens all the time: someone re-runs a failed job, or the push run and the pull-request run of the same commit disagree. flake-doctor lists a repo's workflow runs, fetches the jobs of failed runs (and of passing runs on the same commits), downloads the failed jobs' logs, pulls the pytest failures out of them, and checks which failures were followed by a pass of the same job on the same commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it found
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Runs scanned&lt;/th&gt;
&lt;th&gt;Failed jobs&lt;/th&gt;
&lt;th&gt;Confirmed flaky tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pydantic&lt;/td&gt;
&lt;td&gt;1,241&lt;/td&gt;
&lt;td&gt;1,144&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aiohttp&lt;/td&gt;
&lt;td&gt;4,180 (25 days)&lt;/td&gt;
&lt;td&gt;610&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fastapi&lt;/td&gt;
&lt;td&gt;3,119&lt;/td&gt;
&lt;td&gt;257&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pandas&lt;/td&gt;
&lt;td&gt;2,836 (7 days)&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(aiohttp and pandas run so much CI that the scan hit its 5,000-run cap before reaching 30 days.)&lt;/p&gt;

&lt;h3&gt;
  
  
  aiohttp: the network is a liar
&lt;/h3&gt;

&lt;p&gt;All seven of aiohttp's flaky tests are the kind you'd guess for an async HTTP library:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep-alive timeout tests failing with &lt;code&gt;ClientOSError: [Errno 104] Connection reset by peer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a WebSocket heartbeat test and a stalled-parser test failing with an unraisable exception raised in the finalizer of a &lt;code&gt;WebSocketWriter.send_frame&lt;/code&gt; coroutine&lt;/li&gt;
&lt;li&gt;a read-timeout test hitting &lt;code&gt;SocketTimeoutError&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a timing assertion: &lt;code&gt;assert 0.014999999999986358 == 0 ¬± 0.001&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one failed and then passed on the same commit. The timing one is my favourite: it expects a value of zero, give or take a millisecond, and on that run it came out as 15 ms. Timing assertions and shared CI runners don't mix.&lt;/p&gt;

&lt;h3&gt;
  
  
  pydantic: the test that only fails after midnight
&lt;/h3&gt;

&lt;p&gt;pydantic's best one is a classic. In &lt;code&gt;tests/test_datetime.py&lt;/code&gt;:&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="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2064-06-01&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_past_date_validation_fails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;past_date_type&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;date.today()&lt;/code&gt; inside &lt;code&gt;parametrize&lt;/code&gt; runs when pytest &lt;strong&gt;collects&lt;/strong&gt; the tests, not when it runs them. A macOS job collected the tests on 10 October and ran them after midnight UTC. By then "today" was yesterday, so &lt;code&gt;PastDate&lt;/code&gt; happily accepted it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;value = datetime.date(2026, 10, 10), past_date_type = &amp;lt;class 'pydantic.types.PastDate'&amp;gt;
E       Failed: DID NOT RAISE ValidationError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The re-run, 50 minutes later, passed. This kind of failure never gets investigated: it happens rarely, to someone else's job, late at night, and re-running fixes it.&lt;/p&gt;

&lt;h3&gt;
  
  
  fastapi: the false positive I almost shipped
&lt;/h3&gt;

&lt;p&gt;My first version also had a softer category, "suspect", for tests that fail on several commits across several branches. On fastapi it flagged 6. Four of them failed on 20 commits across 12 branches, all with &lt;code&gt;DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That isn't flakiness. A new anyio release broke every branch at once until fastapi adapted. To tell the two apart, I measured how often the &lt;em&gt;workflow&lt;/em&gt; passed around each test's failures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;workflow pass rate around the failures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;aiohttp's confirmed flaky tests&lt;/td&gt;
&lt;td&gt;68% to 86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fastapi's "suspects"&lt;/td&gt;
&lt;td&gt;13% (the anyio four), 33% (two timeouts)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A flaky test fails &lt;em&gt;occasionally while CI is otherwise green&lt;/em&gt;. A breakage turns &lt;em&gt;everything&lt;/em&gt; red. So now a test is only a suspect if its workflow kept passing most of its other runs. Otherwise it's treated as a breakage and hidden.&lt;/p&gt;

&lt;h3&gt;
  
  
  pandas: most red CI isn't tests at all
&lt;/h3&gt;

&lt;p&gt;pandas had 500 failed jobs in a week and no flaky tests. The jobs that passed on retry had failed for infrastructure reasons: an HTTP 500 while downloading a build tool, a pip build failure, a collection error.&lt;/p&gt;

&lt;p&gt;That was the pattern everywhere. &lt;strong&gt;Most failed CI jobs weren't test failures at all&lt;/strong&gt;: lint, type checks, docs builds, preview-upload bots and infrastructure. Any tool that just counts red builds will mostly be counting those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that surprised me while building it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub's API silently caps filtered run listings at 1,000 results.&lt;/strong&gt; My first "30-day" scans covered 2 to 10 days. flake-doctor now splits the time window until each piece fits under the cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pydantic uses pytest-pretty&lt;/strong&gt;, which replaces pytest's &lt;code&gt;FAILED&lt;/code&gt; summary lines with a table. My parser found 0 test names in 173 failed test jobs until I taught it to read the table, and to fall back on pytest's own failure sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool found a flaky test in its own test suite&lt;/strong&gt; the first time its GitHub Action ran. Two tests can tie perfectly as "the one that breaks the victim", because their positions in a shuffled order are correlated through the victim's position, and my code was picking one of them arbitrarily. Now it reports the tie and hands over to a bisection step that settles it.&lt;/li&gt;
&lt;/ul&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;pipx &lt;span class="nb"&gt;install &lt;/span&gt;flake-doctor
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...     &lt;span class="c"&gt;# read access to the repo's Actions&lt;/span&gt;
flake-doctor scan owner/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a &lt;a href="https://github.com/marketplace/actions/flake-doctor-flaky-test-report" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; that keeps a "Flaky test report" issue up to date, local tools that rerun a test alone and in shuffled order to find &lt;em&gt;why&lt;/em&gt; it's flaky, and a Claude Code plugin. Everything is open source: &lt;a href="https://github.com/ChiragPatankar/Flake-doctor" rel="noopener noreferrer"&gt;github.com/ChiragPatankar/Flake-doctor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run it on your project, I'd love to hear what it finds, and especially where it gets things wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
