Retrying Flaky API Calls in Python
Problem
You're making a request to an external API, but sometimes the server is busy or the network is slow. Instead of getting a response, your program crashes with a connection error. You want to make your program more resilient by automatically trying the request again a few times before giving up.
Clarifying the Issue
Many services are not 100% reliable. A single failed request doesn't always mean the service is down; it could be a temporary issue. Beginners often just let the program crash on the first failure. In production, this can lead to data pipelines failing or applications becoming unresponsive.
Why It Matters
Imagine you're running a script that processes 10,000 items, and you need to send each one to an external service. If the API flakes out for just a second, your entire script fails, and you have to start all over. This isn't just frustrating; it's a huge waste of time and resources. Building a simple retry loop is the difference between a brittle script and a robust, professional tool.
Key Terms
- API (Application Programming Interface): A set of rules that allows applications to talk to each other.
- Exception: An event that interrupts the normal flow of a program's instructions.
- Exponential Backoff: A strategy that waits a progressively longer time between retries to avoid overwhelming a busy server.
- Idempotent: An operation that can be performed multiple times without changing the result beyond the initial application. (This is a good note for readers who want to dive deeper).
Steps at a Glance
- Define the problem in pseudocode.
- Write a human-first Python function that retries the request with a simple
time.sleep()
. - Show the compact Pythonic alternative using a library like
tenacity
. - Test both solutions to demonstrate their resilience.
Detailed Steps
1. Pseudocode
- Take a URL and the number of retries.
- For each retry attempt (starting from 1):
- Try to make the API call.
- If it succeeds, return the result.
- If it fails, wait for a short period.
- If this is the last attempt and it fails, raise the error.
- Otherwise, continue to the next attempt.
2. Human-First Python (Readable)
import requests
import time
def call_api_with_retries(url: str, retries: int = 3) -> dict:
"""
Attempts to call a URL, retrying up to a specified number of times.
"""
for i in range(retries):
try:
print(f"Attempting to call {url} (Attempt {i + 1})...")
response = requests.get(url)
response.raise_for_status() # This will raise an error for bad responses (4xx or 5xx)
return response.json()
except requests.exceptions.RequestException as e:
# We catch a broad exception to handle connection errors, timeouts, etc.
print(f"Request failed: {e}")
if i < retries - 1:
# If it's not the last attempt, we wait and try again.
wait_time = 2 ** i
print(f"Waiting {wait_time} seconds before next retry.")
time.sleep(wait_time)
else:
# If we've run out of retries, re-raise the original error.
raise
return {} # Fallback return if all retries fail
- The
for
loop makes the retry count explicit. - The
try...except
block clearly separates the successful case from the failure case. - Uses a simple exponential backoff (
2 ** i
) to avoid hammering the server. - The
print
statements provide clear feedback on each attempt, which is great for debugging.
3. Pythonic / AI Python (Compact)
from tenacity import retry, stop_after_attempt, wait_exponential
import requests
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def call_api_with_retries_tenacity(url: str) -> dict:
"""
Calls a URL using the tenacity library for automatic retries.
"""
print(f"Attempting to call {url}...")
response = requests.get(url)
response.raise_for_status()
return response.json()
- This approach uses the
tenacity
library, a powerful tool designed specifically for this problem. - The
@retry
decorator abstracts away the entire retry loop. - It handles both the number of retries (
stop_after_attempt
) and the exponential backoff (wait_exponential
) in a single line. - For a seasoned dev, this is the go-to solution—it's clean, declarative, and highly reliable.
Conclusion
A simple retry loop is a small change with a huge impact on your code's resilience. Without it, you’re at the mercy of network errors. By wrapping your API calls, you make your programs far more robust.
You now have two powerful ways to solve this pain point:
- Human-First Python: A step-by-step approach that teaches the underlying logic of a retry loop.
- Pythonic: A compact, idiomatic solution using a specialized library.
Both methods solve the problem perfectly. It's up to you to decide which one fits your team's needs and skill level.
Which approach would you use in your own projects, and why?
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Comments
Post a Comment