🔄 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
Strategy | Use Case | Delay Pattern |
---|---|---|
Fixed Delay | Simple operations | 1s, 1s, 1s |
Exponential Backoff | Network/API calls | 1s, 2s, 4s, 8s |
Linear Backoff | Database operations | 1s, 2s, 3s, 4s |
Random Jitter | High-traffic systems | Random 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
Parameter | Purpose | Typical Values |
---|---|---|
max_attempts | Maximum retry count | 3-5 for most operations |
base_delay | Initial delay | 0.5-2 seconds |
max_delay | Maximum delay | 30-60 seconds |
timeout | Overall timeout | 30-300 seconds |
Exception Categories
Category | Examples | Retry Strategy |
---|---|---|
Transient | ConnectionError , TimeoutError | Retry with backoff |
Rate Limiting | HTTP 429 | Exponential backoff |
Server Errors | HTTP 5xx | Limited retries |
Client Errors | HTTP 4xx , ValueError | Don'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?
Track Your Learning Progress
Sign in to bookmark tutorials and keep track of your learning journey.
Your progress is saved automatically as you read.