<?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: Emre Kadir Dağdelen</title>
    <description>The latest articles on DEV Community by Emre Kadir Dağdelen (@dagdelean).</description>
    <link>https://dev.to/dagdelean</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%2F4026094%2F52f9a3c3-20e6-4328-8c24-2a097300bf82.jpg</url>
      <title>DEV Community: Emre Kadir Dağdelen</title>
      <link>https://dev.to/dagdelean</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dagdelean"/>
    <language>en</language>
    <item>
      <title>Building the Polly that Python never had</title>
      <dc:creator>Emre Kadir Dağdelen</dc:creator>
      <pubDate>Mon, 13 Jul 2026 10:53:15 +0000</pubDate>
      <link>https://dev.to/dagdelean/building-the-polly-that-python-never-had-1olm</link>
      <guid>https://dev.to/dagdelean/building-the-polly-that-python-never-had-1olm</guid>
      <description>&lt;h1&gt;
  
  
  Building the Polly that Python never had
&lt;/h1&gt;

&lt;p&gt;Every app that calls external APIs faces the same four problems: transient&lt;br&gt;
failures, dead dependencies, rate limits, and slow responses. Java solved&lt;br&gt;
this with resilience4j. .NET solved it with Polly. Python never had a&lt;br&gt;
unified answer: tenacity does retries, pybreaker does circuit breaking&lt;br&gt;
(sync only, aging), and everything else you wire by hand.&lt;/p&gt;

&lt;p&gt;These patterns only pay off when they compose: a timeout per attempt,&lt;br&gt;
retries that respect an open circuit, a fallback that absorbs the rest.&lt;br&gt;
Wiring that from three libraries with three philosophies is the code&lt;br&gt;
nobody wants to own. So I built nopanic.&lt;/p&gt;

&lt;h2&gt;
  
  
  One decorator API, sync and async
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from nopanic import compose, fallback, retry, circuit_breaker, timeout, backoff, CircuitOpen

llm = circuit_breaker(failure_threshold=0.5, reset_timeout=20.0, name="llm")

resilient = compose(
    fallback(lambda e: "temporarily unavailable", on=(ConnectionError, CircuitOpen, TimeoutError)),
    retry(attempts=3, on=(ConnectionError, TimeoutError), backoff=backoff.full_jitter(0.2)),
    llm,
    timeout(30.0),
)

@resilient
async def ask(prompt: str) -&amp;gt; str: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Reading inside out: each attempt gets 30 seconds, outcomes feed the&lt;br&gt;
breaker, transient failures retry with jitter, and whatever is left&lt;br&gt;
becomes a degraded answer instead of a traceback. The same decorators&lt;br&gt;
work on plain sync functions. Zero runtime dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design choices that mattered
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No wrapper exceptions.&lt;/strong&gt; Exhausted retries re-raise the original error.&lt;br&gt;
Your &lt;code&gt;except ConnectionError:&lt;/code&gt; keeps working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Injectable time.&lt;/strong&gt; Breakers, rate limiters and caches take a &lt;code&gt;clock=&lt;/code&gt;&lt;br&gt;
parameter, so the test suite (141 tests) runs in under two seconds with&lt;br&gt;
zero sleeps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability as a stream.&lt;/strong&gt; &lt;code&gt;events.subscribe()&lt;/code&gt; sees everything every&lt;br&gt;
policy does. With no listeners the cost is one attribute read (~0.1us).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured overhead.&lt;/strong&gt; Every policy costs 0.15 to 1.3 microseconds per&lt;br&gt;
call on the success path. A fast HTTP round trip is 5,000+ us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation: retrofitting a 232k-star repo
&lt;/h2&gt;

&lt;p&gt;To test the API against reality, I took network scripts from&lt;br&gt;
geekcomputers/Python and added resilience: three decorators per call&lt;br&gt;
site, no restructuring. The exercise even improved the design: my first&lt;br&gt;
retry policy naively retried a 403, which led to the documented&lt;br&gt;
"retry 429/5xx, never other 4xx" recipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that only exists on Windows
&lt;/h2&gt;

&lt;p&gt;The best story came from CI. Tests passed everywhere except Windows on&lt;br&gt;
Python 3.11/3.12. After reproducing locally and instrumenting, the data&lt;br&gt;
showed &lt;code&gt;time.sleep(0.05)&lt;/code&gt; waking after 47ms by the monotonic clock, and&lt;br&gt;
sub-quantum sleeps returning instantly. My retry honored a server's&lt;br&gt;
Retry-After by sleeping exactly that long, then racing the deadline and&lt;br&gt;
losing, burning attempts against a still-closed breaker window.&lt;/p&gt;

&lt;p&gt;The fix belongs in the library, not the test: "retry after X" means "not&lt;br&gt;
before X", so honored hints now sleep X * 1.05 + 50ms, still capped so a&lt;br&gt;
hostile server cannot park your client. If your code races deadlines&lt;br&gt;
exactly, Windows will eventually teach you the same lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adaptive rate limiting: stop hardcoding guesses
&lt;/h2&gt;

&lt;p&gt;The newest addition applies TCP's AIMD congestion control to API quotas:&lt;br&gt;
a 429 cuts your rate multiplicatively, successes recover it additively,&lt;br&gt;
and Retry-After blocks the bucket for exactly that long. You converge on&lt;br&gt;
the rate the server actually sustains.&lt;/p&gt;

&lt;p&gt;pip install nopanic — GitHub: github.com/dagdelenemre/nopanic&lt;br&gt;
Feedback and issues welcome, especially from Polly and resilience4j users.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
