<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://pablo.ie/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://pablo.ie/" rel="alternate" type="text/html" /><updated>2026-10-02T19:41:44+00:00</updated><id>https://pablo.ie/blog/feed.xml</id><title type="html">Pablo Nicolás Estevez</title><subtitle>Backend Python Developer based in Dublin, Ireland.</subtitle><author><name>Pablo Nicolás Estevez</name></author><entry><title type="html">How I ended up maintaining an aiohttp testing library</title><link href="https://pablo.ie/blog/aiointercept" rel="alternate" type="text/html" title="How I ended up maintaining an aiohttp testing library" /><published>2026-10-02T00:00:00+00:00</published><updated>2026-10-02T00:00:00+00:00</updated><id>https://pablo.ie/blog/aiointercept</id><content type="html" xml:base="https://pablo.ie/blog/aiointercept"><![CDATA[<p>This year I started to write and maintain <a href="https://pypi.org/project/aiointercept/">aiointercept</a>, a library to mock aiohttp. This is the story of how that happened, and everything related to it.</p>

<p><a href="https://github.com/pnuckowski/aioresponses">aioresponses</a> is the most popular library in the ecosystem for this task. I started to use it professionally and I had a few pain points, and development had stalled, so they were not getting fixed. First I tried to help on the library itself. Then I tried to move it to other maintainers, so it would not be lost. In the end I received advice on how to build a library with a better approach, and I built it.</p>

<h2 id="aioresponses-and-why-it-breaks">aioresponses and why it breaks</h2>

<p>The first commit of aioresponses is from 17 October 2016. The way it works is simple: it creates aiohttp’s <code class="language-plaintext highlighter-rouge">ClientResponse</code> objects by hand and gives them to your code, without any real request.</p>

<p>The problem is that <code class="language-plaintext highlighter-rouge">ClientResponse</code> is internal to aiohttp. The aiohttp documentation says users never create it. So every time aiohttp changes that class, aioresponses breaks.</p>

<p>This happened early. On 20 April 2017 an aiohttp release broke aioresponses for the first time (<a href="https://github.com/pnuckowski/aioresponses/pull/59/changes">PR #59</a>). In June 2018 the response changed again (<a href="https://github.com/pnuckowski/aioresponses/pull/98">PR #98</a>). I don’t think this is anybody’s fault. It is the approach: if you depend on the internals of another library, you will break when they change.</p>

<h2 id="using-aioresponses-and-trying-to-help">Using aioresponses and trying to help</h2>

<p>I started to use aioresponses professionally in April 2025. The API is nice and I liked it. But I had some pain points. One was that failed assertions were hard to read, so on 4 September 2025 I asked for better formatting in <a href="https://github.com/pnuckowski/aioresponses/issues/276">issue #276</a>.</p>

<p>Nothing moved for months. In March 2026 I asked directly whether the project was maintained (<a href="https://github.com/pnuckowski/aioresponses/issues/281">issue #281</a>). On 3 March I also suggested moving the repository to aio-libs (<a href="https://github.com/orgs/aio-libs/discussions/45">discussion #45</a>). I think this is the correct thing to do before forking or writing something new: ask first.</p>

<p>The answer from aio-libs was that they could host it, if there was someone interested in maintaining it. But <a href="https://github.com/Dreamsorcerer">Dreamsorcerer</a> and <a href="https://github.com/webknjaz">webknjaz</a> also told me that the aiohttp maintainers advise against this kind of mocking. They recommend sending real requests to a test server. At first I thought that was not practical. Later I understood they were right.</p>

<h2 id="writing-aiointercept">Writing aiointercept</h2>

<p>On 13 March 2026 I did a proof of concept: an API like aioresponses, but using a real server. The client makes a real request, with real headers and real serialization, and a local <code class="language-plaintext highlighter-rouge">aiohttp.web</code> server answers it. Nothing depends on aiohttp internals, so there is nothing for aiohttp to break.</p>

<p>It worked faster than I expected, and that started to convince me.</p>

<p>Meanwhile, aioresponses became active again, and I want to be fair about that. <a href="https://github.com/pnuckowski">pnuckowski</a> merged a PR of mine on 12 April (<a href="https://github.com/pnuckowski/aioresponses/pull/280">PR #280</a>) and said the next day that the project was maintained. I continued helping there. In April I also suggested changes to how redirects work (<a href="https://github.com/pnuckowski/aioresponses/issues/283">issue #283</a>).</p>

<p>Version 0.1.0 of aiointercept went up on PyPI on 16 April. On 4 May I released 0.1.1 and showed it to webknjaz. webknjaz replied with a list of things I was doing wrong in my packaging and CI. That helped me a lot.</p>

<p>On 23 May, with version 0.1.4, I told aio-libs that the project was working. I hoped it would be useful to other people, but I did not expect any success.</p>

<h2 id="the-aiohttp-314-break-june-2026">The aiohttp 3.14 break (June 2026)</h2>

<p>At the start of June 2026 aiohttp released 3.14, and it was incompatible with aioresponses. <code class="language-plaintext highlighter-rouge">ClientResponse</code> got a new required argument, <code class="language-plaintext highlighter-rouge">stream_writer</code>, and aioresponses did not pass it (<a href="https://github.com/pnuckowski/aioresponses/issues/289">issue #289</a>). The same problem as 2017, 2018 and other times.</p>

<p>There was a quick workaround, and some projects used it and still use it. But it means patching aioresponses, and if you have to patch your mocking library, that is not a good sign. Also, 3.14 was a security update (<a href="https://github.com/advisories/GHSA-jg22-mg44-37j8">GHSA-jg22-mg44-37j8</a>), so staying on the old aiohttp was not a good option. A test library should never be the reason you cannot install a security fix.</p>

<p>On 5 June somebody opened an issue on aiohttp about the incompatibility (<a href="https://github.com/aio-libs/aiohttp/issues/12815">issue #12815</a>). Some people said a minor version should not break things. The maintainers answered that nobody is supposed to create that class, and I agree with them. If every internal thing that somebody uses becomes a promise, a library can never change.</p>

<p>In that issue <a href="https://github.com/bdraco">bdraco</a> and Dreamsorcerer suggested aiointercept as the answer. I was surprised. My library was two months old and it was being recommended in the aiohttp tracker.</p>

<p>After that things moved fast:</p>

<ul>
  <li>9 June: the first pull request from another person, <a href="https://github.com/agroebe">agroebe</a>, was merged and released as 0.1.6.</li>
  <li>11 June: a couple of projects migrated and surfaced issues from use cases I had not covered.</li>
  <li>12 June: A new release, with 6 changes in just 3 days, and a second contributor.</li>
</ul>

<h2 id="what-happened-after">What happened after</h2>

<p>The first migrations taught me a lot. Every project used mocks in a different way, and each one found something I had not thought about. Some issues were bugs in aiointercept. Others were bugs in their tests that the fake responses had been hiding. I think finding those is a good thing, and it is what really convinced me: real projects had bugs that were covered by the mocking approach of aioresponses.</p>

<p>One example is redirects: aioresponses does not use the same redirect machinery as aiohttp (<a href="https://github.com/pnuckowski/aioresponses/issues/283">issue #283</a>). Another is traces, an old aioresponses issue (<a href="https://github.com/pnuckowski/aioresponses/issues/246">issue #246</a>) that one project fixed on its side when it moved to aiointercept (<a href="https://github.com/epam/ai-dial-sdk/pull/411">ai-dial-sdk PR #411</a>).</p>

<p>In one project, the code expected a specific exception when the server answered with a 500. In reality it would receive a <code class="language-plaintext highlighter-rouge">ClientResponse</code> with status 500 and no exception. The test was passing only because they were giving that same exception to aioresponses to raise. The mock was confirming something that never happens.</p>

<p>With a real server these things show up, because it is aiohttp itself doing the work.</p>

<p>The latest release of aioresponses is from 23 June 2026. The same day a fork appeared, <a href="https://pypi.org/project/aioresponses-ng/">aioresponses-ng</a>. It has the same approach as aioresponses, so I think it can break in the same way. But I like that there is a maintained fork for the people who want to keep their tests as they are, and I hope it stays maintained.</p>

<p>In July 2026 I stopped using aiohttp professionally. It is a bit funny, because that was the month the downloads started to grow. That did not change anything for the project: I still maintain aiointercept, and I am still taking part in the aio-libs community.</p>

<h2 id="pypi-statistics">PyPI statistics</h2>

<p>aiointercept has about 405,000 downloads since the first upload on 16 April 2026. It passed 100,000 a month in July. The growth started the week after aiohttp 3.14, and the busiest day was 29 July with about 17,700 downloads.</p>

<figure class="bar-chart" aria-label="aiointercept monthly downloads, April to September 2026">
  <figcaption>aiointercept downloads per month, mirrors excluded</figcaption>
  <div class="bar-chart-plot">
    <div class="bar-col" tabindex="0" data-tip="Apr 2026 · 120 (from 16 Apr)"><div class="bar" style="height:0.08%"></div><span class="bar-x">Apr</span></div>
    <div class="bar-col" tabindex="0" data-tip="May 2026 · 635"><div class="bar" style="height:0.44%"></div><span class="bar-x">May</span></div>
    <div class="bar-col" tabindex="0" data-tip="Jun 2026 · 15,474"><div class="bar" style="height:10.7%"></div><span class="bar-x">Jun</span></div>
    <div class="bar-col" tabindex="0" data-tip="Jul 2026 · 109,343"><div class="bar" style="height:75.4%"></div><span class="bar-x">Jul</span></div>
    <div class="bar-col" tabindex="0" data-tip="Aug 2026 · 144,991"><div class="bar" style="height:100%"><span class="bar-label">145k</span></div><span class="bar-x">Aug</span></div>
    <div class="bar-col" tabindex="0" data-tip="Sep 2026 · 125,932"><div class="bar" style="height:86.9%"></div><span class="bar-x">Sep</span></div>
  </div>
</figure>

<details>
  <summary>Show the numbers</summary>

  <table>
    <thead>
      <tr>
        <th>Month</th>
        <th style="text-align: right">Downloads</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Apr 2026 (from 16 Apr)</td>
        <td style="text-align: right">120</td>
      </tr>
      <tr>
        <td>May 2026</td>
        <td style="text-align: right">635</td>
      </tr>
      <tr>
        <td>Jun 2026</td>
        <td style="text-align: right">15,474</td>
      </tr>
      <tr>
        <td>Jul 2026</td>
        <td style="text-align: right">109,343</td>
      </tr>
      <tr>
        <td>Aug 2026</td>
        <td style="text-align: right">144,991</td>
      </tr>
      <tr>
        <td>Sep 2026</td>
        <td style="text-align: right">125,932</td>
      </tr>
    </tbody>
  </table>

</details>

<p>To be honest about the size: aioresponses is still about 44 times bigger, with around 5.6 million downloads in September. It went down from about 14.5 million in May, but I can’t say that is because of aiointercept. My library went from 0.1% of its downloads in June to 2.3% in September, which is only a small part of that drop.</p>

<p>Also, downloads are mostly CI machines, not people. What the numbers tell me is that a few projects with a lot of CI moved to aiointercept and stayed. I am happy with that.</p>

<p class="small">Sources: pypistats.org for <a href="https://pypistats.org/packages/aiointercept">aiointercept</a>, <a href="https://pypistats.org/packages/aioresponses">aioresponses</a> and <a href="https://pypistats.org/packages/aioresponses-ng">aioresponses-ng</a>; release dates from <a href="https://pypi.org/project/aiointercept/#history">PyPI</a>.</p>

<h2 id="what-i-learned">What I learned</h2>

<p>This whole history taught me a lot: about aiohttp, about packaging and releasing, about how other people test their code, and about what it means to maintain something that others depend on.</p>

<p>I expect to contribute more to the Python ecosystem in general, and to aio-libs in particular.</p>

<details>
  <summary>Full timeline</summary>

  <table>
    <thead>
      <tr>
        <th>Date</th>
        <th>Event</th>
        <th>Link</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>28 Sep 2026</td>
        <td>aiointercept 0.1.12, latest release</td>
        <td><a href="https://pypi.org/project/aiointercept/#history">PyPI</a></td>
      </tr>
      <tr>
        <td>Jul 2026</td>
        <td>I stop using aiohttp professionally</td>
        <td> </td>
      </tr>
      <tr>
        <td>5 Jul 2026</td>
        <td>Last release of aioresponses-ng</td>
        <td> </td>
      </tr>
      <tr>
        <td>23 Jun 2026</td>
        <td>Latest release of aioresponses; first release of aioresponses-ng</td>
        <td> </td>
      </tr>
      <tr>
        <td>Jun 2026</td>
        <td>I open another aioresponses fix</td>
        <td><a href="https://github.com/pnuckowski/aioresponses/pull/294">PR #294</a></td>
      </tr>
      <tr>
        <td>11 Jun 2026</td>
        <td>A couple of projects migrate and find issues in new use cases</td>
        <td> </td>
      </tr>
      <tr>
        <td>9 Jun 2026</td>
        <td>First PR from another person (agroebe) merged and released as 0.1.6</td>
        <td> </td>
      </tr>
      <tr>
        <td>5 Jun 2026</td>
        <td>Incompatibility raised on aiohttp; bdraco and Dreamsorcerer recommend aiointercept</td>
        <td><a href="https://github.com/aio-libs/aiohttp/issues/12815">aiohttp #12815</a></td>
      </tr>
      <tr>
        <td>Early Jun 2026</td>
        <td>aiohttp 3.14, a security update, breaks aioresponses</td>
        <td><a href="https://github.com/pnuckowski/aioresponses/issues/289">issue #289</a>, <a href="https://github.com/advisories/GHSA-jg22-mg44-37j8">advisory</a></td>
      </tr>
      <tr>
        <td>23 May 2026</td>
        <td>I tell aio-libs the project is working (0.1.4)</td>
        <td> </td>
      </tr>
      <tr>
        <td>4 May 2026</td>
        <td>aiointercept 0.1.1; shown to webknjaz</td>
        <td> </td>
      </tr>
      <tr>
        <td>16 Apr 2026</td>
        <td>aiointercept 0.1.0 uploaded to PyPI</td>
        <td><a href="https://pypi.org/project/aiointercept/#history">PyPI</a></td>
      </tr>
      <tr>
        <td>Apr 2026</td>
        <td>I suggest redirect changes in aioresponses</td>
        <td><a href="https://github.com/pnuckowski/aioresponses/issues/283">issue #283</a></td>
      </tr>
      <tr>
        <td>13 Apr 2026</td>
        <td>pnuckowski says aioresponses is maintained</td>
        <td> </td>
      </tr>
      <tr>
        <td>12 Apr 2026</td>
        <td>pnuckowski merges a PR I authored</td>
        <td><a href="https://github.com/pnuckowski/aioresponses/pull/280">PR #280</a></td>
      </tr>
      <tr>
        <td>13 Mar 2026</td>
        <td>Proof of concept: an aioresponses-like API on a real server</td>
        <td> </td>
      </tr>
      <tr>
        <td>3 Mar 2026</td>
        <td>I propose moving aioresponses to aio-libs</td>
        <td><a href="https://github.com/orgs/aio-libs/discussions/45">discussion #45</a></td>
      </tr>
      <tr>
        <td>Mar 2026</td>
        <td>I ask whether aioresponses is maintained</td>
        <td><a href="https://github.com/pnuckowski/aioresponses/issues/281">issue #281</a></td>
      </tr>
      <tr>
        <td>4 Sep 2025</td>
        <td>I ask for better-formatted assertions</td>
        <td><a href="https://github.com/pnuckowski/aioresponses/issues/276">issue #276</a></td>
      </tr>
      <tr>
        <td>Apr 2025</td>
        <td>I start using aioresponses professionally</td>
        <td> </td>
      </tr>
      <tr>
        <td>Jun 2018</td>
        <td>aiohttp’s response changes again</td>
        <td><a href="https://github.com/pnuckowski/aioresponses/pull/98">PR #98</a></td>
      </tr>
      <tr>
        <td>20 Apr 2017</td>
        <td>First aiohttp release that breaks aioresponses</td>
        <td><a href="https://github.com/pnuckowski/aioresponses/pull/59/changes">PR #59</a></td>
      </tr>
      <tr>
        <td>17 Oct 2016</td>
        <td>First aioresponses commit</td>
        <td> </td>
      </tr>
    </tbody>
  </table>

</details>]]></content><author><name>Pablo Nicolás Estevez</name></author><summary type="html"><![CDATA[The story of aiointercept, from pain points with aioresponses to a library recommended in the aiohttp tracker.]]></summary></entry></feed>