Using Tenacity to ‘Resurrect’ Python Apps: Mastering API and Database Error Handling

Python tutorial - IT technology blog
Python tutorial - IT technology blog

When Reality Doesn’t Meet Expectations

At three o’clock on a Monday morning, my phone vibrated incessantly. My data crawling bot, which had been running smoothly, suddenly “dropped dead.” Checking the logs, I only saw a single error line: ConnectionTimeout. In the local environment, everything was 100% perfect. However, once deployed to the server, it would act up about three times a week because a partner’s API was slow to respond.

This project started with just 200 lines of code. After three months of operation, it ballooned to over 2,000 lines. Sadly, half of that wasn’t business logic; it was patchwork code to handle flaky network connections. This was a costly lesson in building resilient systems that every developer must go through.

Why Does Your Code Keep Crashing?

In the world of distributed systems, we face Transient Faults (temporary errors). These are errors that come and go on their own. If you retry after a few seconds, there’s a high chance everything will run smoothly again. Classic scenarios include:

  • Micro-outages in internet connection.
  • Partner server overloaded, returning 429 (Rate limiting).
  • Database temporarily locked for scheduled backups.
  • Microservices responding slowly due to bandwidth congestion.

Writing “happy path” code is a fatal mistake. Just a 1-2 second network lag can cause the entire downstream process to collapse like a house of cards. You can’t control the network infrastructure, but you can control how your code reacts to it.

Common “Clunky” Error Handling Approaches

Approach 1: While Loops and Try-Except (Code Smell)

This is the most instinctive method. Almost everyone learning Python has written something like this:

import time

attempts = 0
while attempts < 3:
    try:
        result = call_api_service()
        break
    except Exception as e:
        attempts += 1
        print(f"Retrying attempt {attempts}...")
        time.sleep(2)
else:
    print("Total failure.")

This code works, but it’s very “dirty.” Imagine having 50 endpoints to call. Your codebase would be drowning in while loops and counter variables. It dilutes the core logic, is extremely hard to maintain, and easily leads to logical errors—like forgetting to increment the counter, resulting in an infinite loop.

Approach 2: Writing Custom Decorators (Still Not Optimal)

A bit better, you could write your own decorator for reuse. However, manually handling things like Exponential Backoff (gradual wait increases) or filtering specific Exception types for retries is a nightmare. Don’t waste time reinventing the wheel when the community has optimized these solutions for years.

The Professional Solution: The Tenacity Library

Tenacity is a Python library born to solve the retry problem elegantly. Instead of writing imperative control code, you simply declare your requirements using Decorators.

Quick Installation

pip install tenacity

1. Basic Retry Mechanism

Add @retry to the top of your function, and you’re done! By default, it will retry indefinitely until the function succeeds.

from tenacity import retry

@retry
def do_something_unreliable():
    print("Attempting to connect...")
    raise Exception("Network error!")

2. Controlling Retries and Timeouts

In reality, we can’t wait forever. You need to set limits for your application’s tolerance.

from tenacity import retry, stop_after_attempt, stop_after_delay

# Retry a maximum of 5 times
@retry(stop=stop_after_attempt(5))
def call_api():
    raise IOError("Fail")

# Stop after 10 seconds regardless of the number of attempts
@retry(stop=stop_after_delay(10))
def connect_db():
    pass

3. Smart Wait Strategies

Don’t retry immediately! If the server is overloaded and you spam requests, your IP will be blocked instantly. Use Exponential Backoff.

from tenacity import retry, wait_exponential

# 1st wait 1s, 2nd wait 2s, 3rd wait 4s... max 10s
@retry(wait=wait_exponential(multiplier=1, min=1, max=10))
def fetch_data():
    raise Exception("Server busy")

4. Retrying Only When Necessary

If you encounter a 404 Not Found error, retrying 1,000 times won’t help. Tenacity allows you to filter exactly which error types should trigger a retry.

import requests
from tenacity import retry, retry_if_exception_type

@retry(retry=retry_if_exception_type(requests.exceptions.ConnectionError))
def get_weather():
    return requests.get("https://api.weather.com/v1/...")

Real-World Application: Building a Robust API Call Function

This is the code snippet I often use for OpenAI API calls or electronic payments. It incorporates logging so you can easily track what the system is doing behind the scenes.

import logging
import requests
from tenacity import retry, stop_after_attempt, wait_fixed, before_sleep_log, retry_if_exception

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def is_transient_error(exception):
    if isinstance(exception, requests.exceptions.HTTPError):
        # Only retry for 429 (Rate limit) or 5xx server errors
        return exception.response.status_code in [429, 500, 502, 503, 504]
    return isinstance(exception, (requests.exceptions.ConnectionError, requests.exceptions.Timeout))

@retry(
    stop=stop_after_attempt(3), 
    wait=wait_fixed(2), 
    before_sleep=before_sleep_log(logger, logging.INFO),
    retry=retry_if_exception(is_transient_error)
)
def safe_get_data(url):
    response = requests.get(url, timeout=5)
    response.raise_for_status() 
    return response.json()

try:
    data = safe_get_data("https://api.example.com/data")
    print("Data retrieved successfully!")
except Exception as e:
    print(f"Failed after 3 attempts: {e}")

Conclusion

The difference between a Senior and a Junior developer lies in how they handle imperfect situations. Using Tenacity not only makes your code cleaner (Clean Code) but also gives your system magical self-healing capabilities. Start applying it to your projects today. Trust me, you’ll sleep better without those midnight error calls!

Share: