🔄 Implementing Retry Logic

Retry logic helps your applications handle temporary failures gracefully by automatically retrying failed operations. This is essential for network requests, database connections, and other operations that might fail temporarily.

import time
import random

def unreliable_operation():
    # Simulate operation that fails sometimes
    if random.random() < 0.7:  # 70% chance of failure
        raise ConnectionError("Network timeout")
    return "Operation successful"

def simple_retry(max_attempts=3):
    for attempt in range(max_attempts):
        try:
            result = unreliable_operation()
            print(f"Success on attempt {attempt + 1}")
            return result
        except ConnectionError as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            if attempt == max_attempts - 1:
                raise  # Re-raise on final attempt
            time.sleep(1)  # Wait before retry

# Test retry logic
try:
    result = simple_retry()
    print(f"Final result: {result}")
except ConnectionError:
    print("All retry attempts failed")

🎯 Basic Retry Patterns

Different retry strategies for different situations.

Fixed Delay Retry

import time

def fixed_delay_retry(operation, max_attempts=3, delay=1):
    """Retry with fixed delay between attempts"""
    for attempt in range(max_attempts):
        try:
            return operation()
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            
            if attempt == max_attempts - 1:
                print("All attempts exhausted")
                raise
            
            print(f"Waiting {delay} seconds before retry...")
            time.sleep(delay)

def flaky_database_call():
    # Simulate database call that sometimes fails
    import random
    if random.random() < 0.6:
        raise ConnectionError("Database connection lost")
    return {"data": "User profile"}

# Test fixed delay retry
try:
    result = fixed_delay_retry(flaky_database_call, max_attempts=3, delay=2)
    print(f"Database result: {result}")
except ConnectionError:
    print("Database operation failed permanently")

Exponential Backoff

import time

def exponential_backoff_retry(operation, max_attempts=3, base_delay=1):
    """Retry with exponentially increasing delays"""
    for attempt in range(max_attempts):
        try:
            return operation()
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            
            if attempt == max_attempts - 1:
                raise
            
            # Calculate exponential delay: base_delay * 2^attempt
            delay = base_delay * (2 ** attempt)
            print(f"Waiting {delay} seconds before retry...")
            time.sleep(delay)

def api_call():
    import random
    if random.random() < 0.8:
        raise TimeoutError("API request timeout")
    return {"status": "success", "data": [1, 2, 3]}

# Test exponential backoff
try:
    result = exponential_backoff_retry(api_call, max_attempts=4, base_delay=0.5)
    print(f"API result: {result}")
except TimeoutError:
    print("API call failed after all retries")

Retry with Custom Conditions

def conditional_retry(operation, max_attempts=3, retryable_exceptions=None):
    """Retry only for specific exception types"""
    if retryable_exceptions is None:
        retryable_exceptions = (ConnectionError, TimeoutError)
    
    for attempt in range(max_attempts):
        try:
            return operation()
        except retryable_exceptions as e:
            print(f"Retryable error on attempt {attempt + 1}: {e}")
            
            if attempt == max_attempts - 1:
                raise
            
            time.sleep(1)
        except Exception as e:
            print(f"Non-retryable error: {e}")
            raise  # Don't retry for other exceptions

def mixed_failure_operation():
    import random
    failure_type = random.random()
    
    if failure_type < 0.4:
        raise ConnectionError("Network issue")  # Retryable
    elif failure_type < 0.6:
        raise ValueError("Invalid input data")  # Not retryable
    else:
        return "Success"

# Test conditional retry
try:
    result = conditional_retry(mixed_failure_operation)
    print(f"Result: {result}")
except Exception as e:
    print(f"Operation failed: {e}")

📋 Retry Strategy Reference

StrategyUse CaseDelay Pattern
Fixed DelaySimple operations1s, 1s, 1s
Exponential BackoffNetwork/API calls1s, 2s, 4s, 8s
Linear BackoffDatabase operations1s, 2s, 3s, 4s
Random JitterHigh-traffic systemsRandom variation

🔧 Advanced Retry Patterns

Retry Decorator

import time
import functools

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """Decorator for automatic retry"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts - 1:
                        raise
                    print(f"Retry {attempt + 1}/{max_attempts} for {func.__name__}: {e}")
                    time.sleep(delay)
        return wrapper
    return decorator

# Use retry decorator
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError,))
def download_file(url):
    import random
    if random.random() < 0.7:
        raise ConnectionError(f"Failed to connect to {url}")
    return f"Downloaded content from {url}"

# Test decorated function
try:
    content = download_file("https://api.example.com/data")
    print(content)
except ConnectionError:
    print("Download failed after all retries")

Retry with Timeout

import time

def retry_with_timeout(operation, max_attempts=3, timeout=10):
    """Retry with overall timeout limit"""
    start_time = time.time()
    
    for attempt in range(max_attempts):
        # Check if we've exceeded the timeout
        if time.time() - start_time > timeout:
            raise TimeoutError(f"Operation timed out after {timeout} seconds")
        
        try:
            return operation()
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            
            if attempt == max_attempts - 1:
                raise
            
            # Don't sleep if it would exceed timeout
            remaining_time = timeout - (time.time() - start_time)
            if remaining_time > 1:
                time.sleep(1)

def slow_operation():
    import random
    time.sleep(random.uniform(0.5, 2))  # Simulate slow operation
    if random.random() < 0.8:
        raise RuntimeError("Operation failed")
    return "Operation completed"

# Test retry with timeout
try:
    result = retry_with_timeout(slow_operation, max_attempts=5, timeout=8)
    print(f"Result: {result}")
except Exception as e:
    print(f"Operation failed: {e}")

Circuit Breaker Pattern

import time

class CircuitBreaker:
    def __init__(self, failure_threshold=3, timeout=10):
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = "CLOSED"  # CLOSED, OPEN, HALF_OPEN
    
    def call(self, operation):
        if self.state == "OPEN":
            if time.time() - self.last_failure_time > self.timeout:
                self.state = "HALF_OPEN"
                print("Circuit breaker: Attempting half-open")
            else:
                raise RuntimeError("Circuit breaker is OPEN")
        
        try:
            result = operation()
            if self.state == "HALF_OPEN":
                self.state = "CLOSED"
                self.failure_count = 0
                print("Circuit breaker: Reset to CLOSED")
            return result
        
        except Exception as e:
            self.failure_count += 1
            self.last_failure_time = time.time()
            
            if self.failure_count >= self.failure_threshold:
                self.state = "OPEN"
                print(f"Circuit breaker: OPENED after {self.failure_count} failures")
            
            raise

def unreliable_service():
    import random
    if random.random() < 0.9:  # 90% failure rate
        raise ConnectionError("Service unavailable")
    return "Service response"

# Test circuit breaker
circuit_breaker = CircuitBreaker(failure_threshold=2, timeout=5)

for i in range(6):
    try:
        result = circuit_breaker.call(unreliable_service)
        print(f"Call {i+1}: {result}")
    except Exception as e:
        print(f"Call {i+1} failed: {e}")
    
    time.sleep(1)

📊 Retry Configuration Guide

Retry Parameters

ParameterPurposeTypical Values
max_attemptsMaximum retry count3-5 for most operations
base_delayInitial delay0.5-2 seconds
max_delayMaximum delay30-60 seconds
timeoutOverall timeout30-300 seconds

Exception Categories

CategoryExamplesRetry Strategy
TransientConnectionError, TimeoutErrorRetry with backoff
Rate LimitingHTTP 429Exponential backoff
Server ErrorsHTTP 5xxLimited retries
Client ErrorsHTTP 4xx, ValueErrorDon't retry

🎯 Key Takeaways

🚀 What's Next?

Congratulations! You've mastered Python's error handling techniques. You're now ready to explore functions and modules for better code organization.

Continue to: Functions and Modules to learn about code organization and reusability.

Was this helpful?

😔Poor
🙁Fair
😊Good
😄Great
🤩Excellent