Skip to content

Instantly share code, notes, and snippets.

@willgarcia
Created December 31, 2025 11:46
Show Gist options
  • Select an option

  • Save willgarcia/056a718b38e1362c8c20225edcd548d0 to your computer and use it in GitHub Desktop.

Select an option

Save willgarcia/056a718b38e1362c8c20225edcd548d0 to your computer and use it in GitHub Desktop.

API Retry Patterns

Collection of Python implementations for handling API calls with token refresh retry logic.

Problem

When processing multiple items through an API, authentication tokens can expire mid-process. This requires:

  • Looping through items
  • Retrying API calls when tokens expire
  • Refreshing tokens between retries
  • Avoiding nested loop complexity

Implementations

1. Traditional Nested Loops (api_retry_loop.py)

Baseline implementation with explicit nested loops.

Pros: Clear control flow
Cons: Nested structure can be hard to follow

2. Extracted Function (api_retry_simple.py)

Separates retry logic into a dedicated function.

Pros: Better separation of concerns
Cons: Still uses nested structure internally

3. Early Exit Pattern (api_retry_flat.py)

Uses break and continue to flatten control flow.

Pros: Simplest to understand
Cons: Still has explicit loops

4. Generator Pattern (api_retry_generator.py)

Abstracts retry attempts through a generator function.

Pros: More readable retry logic
Cons: Adds abstraction layer

5. Recursive Approach (api_retry_recursive.py)

Eliminates loops entirely using recursion.

Pros: Functional programming style, no explicit loops
Cons: Stack depth concerns for many retries

6. Decorator Pattern (api_retry_decorator.py)

Encapsulates retry logic in a reusable decorator.

Pros: Most reusable, clean separation
Cons: Slightly more complex setup

7. Functional with Itertools (api_retry_itertools.py)

Uses map() and itertools for functional approach.

Pros: Functional style, no explicit top-level loops
Cons: Less familiar to some developers

8. Library-Based (api_retry_tenacity.py)

Uses the tenacity library for declarative retry logic.

Pros: Industry standard, handles edge cases, battle-tested
Cons: External dependency

Requires: pip install tenacity

Recommendations

  • For production: api_retry_tenacity.py - Industry standard with robust error handling
  • For learning: api_retry_flat.py - Easiest to understand
  • For reusability: api_retry_decorator.py - Apply to any API function
  • For functional style: api_retry_recursive.py or api_retry_itertools.py

Usage

All implementations follow the same interface:

results = process_items_with_retry(
    items=items,
    api_call_func=make_api_call,
    get_token_func=get_auth_token,
    max_retries=3,
    retry_delay=2
)

Each returns a list of result dictionaries:

{
    'item': <original_item>,
    'response': <api_response>,  # on success
    'error': <error_message>,    # on failure
    'status': 'success' | 'failed'
}
import time
import requests
from typing import List, Callable, Any
from functools import wraps
def with_token_retry(max_retries: int = 3, retry_delay: int = 1):
"""
Decorator that adds token refresh retry logic to API calls.
Args:
max_retries: Maximum number of retry attempts
retry_delay: Delay in seconds between retries
Returns:
Decorated function with retry logic
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(item: Any, token_holder: dict, get_token_func: Callable) -> dict:
last_error = None
for attempt in range(max_retries):
try:
response = func(item, token_holder['token'])
return {'item': item, 'response': response, 'status': 'success'}
except requests.exceptions.HTTPError as e:
last_error = e
if e.response.status_code not in [401, 403] or attempt == max_retries - 1:
return {'item': item, 'error': str(e), 'status': 'failed'}
print(f"Token expired for item {item}. Refreshing token (attempt {attempt + 1}/{max_retries})")
time.sleep(retry_delay)
token_holder['token'] = get_token_func()
except Exception as e:
return {'item': item, 'error': str(e), 'status': 'failed'}
return {'item': item, 'error': str(last_error), 'status': 'failed'}
return wrapper
return decorator
@with_token_retry(max_retries=3, retry_delay=1)
def call_api(item: dict, token: str) -> dict:
"""Make API call with token"""
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'https://api.example.com/endpoint',
json=item,
headers=headers
)
response.raise_for_status()
return response.json()
def process_items(
items: List[Any],
get_token_func: Callable,
api_call_func: Callable = call_api
) -> List[dict]:
"""
Process items with decorated API call function.
Args:
items: List of items to process
get_token_func: Function that retrieves a fresh token
api_call_func: Decorated API call function
Returns:
List of results from API calls
"""
token_holder = {'token': get_token_func()}
return [
api_call_func(item, token_holder, get_token_func)
for item in items
]
# Example usage
def get_auth_token() -> str:
"""Fetch authentication token"""
response = requests.post('https://api.example.com/auth', json={'credentials': 'data'})
return response.json()['token']
if __name__ == '__main__':
items = [
{'id': 1, 'data': 'value1'},
{'id': 2, 'data': 'value2'},
{'id': 3, 'data': 'value3'}
]
results = process_items(
items=items,
get_token_func=get_auth_token
)
for result in results:
if result['status'] == 'success':
print(f"Item {result['item']} processed successfully")
else:
print(f"Item {result['item']} failed: {result['error']}")
import time
import requests
from typing import List, Callable, Any
def process_items_with_retry(
items: List[Any],
api_call_func: Callable,
get_token_func: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> List[dict]:
"""
Process items with API calls that retry on token expiration.
Uses a flat structure with early returns to avoid nested loops.
Args:
items: List of items to process
api_call_func: Function that makes the API call (takes item and token)
get_token_func: Function that retrieves a fresh token
max_retries: Maximum number of retry attempts per item
retry_delay: Delay in seconds between retries
Returns:
List of results from API calls
"""
results = []
token = get_token_func()
for item in items:
attempt = 0
# Keep trying until success or max retries
while attempt < max_retries:
try:
response = api_call_func(item, token)
results.append({'item': item, 'response': response, 'status': 'success'})
break # Success, move to next item
except requests.exceptions.HTTPError as e:
# Token expired, refresh and retry
if e.response.status_code in [401, 403]:
attempt += 1
if attempt >= max_retries:
results.append({'item': item, 'error': str(e), 'status': 'failed'})
break
print(f"Token expired for item {item}. Refreshing token (attempt {attempt}/{max_retries})")
time.sleep(retry_delay)
token = get_token_func()
continue
# Other HTTP error, don't retry
results.append({'item': item, 'error': str(e), 'status': 'failed'})
break
except Exception as e:
# Unexpected error, don't retry
results.append({'item': item, 'error': str(e), 'status': 'failed'})
break
return results
# Example usage
def get_auth_token() -> str:
"""Fetch authentication token"""
response = requests.post('https://api.example.com/auth', json={'credentials': 'data'})
return response.json()['token']
def make_api_call(item: dict, token: str) -> dict:
"""Make API call with token"""
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'https://api.example.com/endpoint',
json=item,
headers=headers
)
response.raise_for_status()
return response.json()
if __name__ == '__main__':
items = [
{'id': 1, 'data': 'value1'},
{'id': 2, 'data': 'value2'},
{'id': 3, 'data': 'value3'}
]
results = process_items_with_retry(
items=items,
api_call_func=make_api_call,
get_token_func=get_auth_token,
max_retries=3,
retry_delay=2
)
for result in results:
if result['status'] == 'success':
print(f"Item {result['item']} processed successfully")
else:
print(f"Item {result['item']} failed: {result['error']}")
import time
import requests
from typing import List, Callable, Any, Iterator
def retry_attempts(max_retries: int) -> Iterator[int]:
"""Generate retry attempt numbers"""
for attempt in range(max_retries):
yield attempt
def process_items_with_retry(
items: List[Any],
api_call_func: Callable,
get_token_func: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> List[dict]:
"""
Process items with API calls using generator pattern to avoid nested loops.
Args:
items: List of items to process
api_call_func: Function that makes the API call (takes item and token)
get_token_func: Function that retrieves a fresh token
max_retries: Maximum number of retry attempts per item
retry_delay: Delay in seconds between retries
Returns:
List of results from API calls
"""
results = []
token = get_token_func()
for item in items:
result = None
for attempt in retry_attempts(max_retries):
try:
response = api_call_func(item, token)
result = {'item': item, 'response': response, 'status': 'success'}
break
except requests.exceptions.HTTPError as e:
if e.response.status_code not in [401, 403]:
result = {'item': item, 'error': str(e), 'status': 'failed'}
break
if attempt == max_retries - 1:
result = {'item': item, 'error': str(e), 'status': 'failed'}
break
print(f"Token expired for item {item}. Refreshing token (attempt {attempt + 1}/{max_retries})")
time.sleep(retry_delay)
token = get_token_func()
except Exception as e:
result = {'item': item, 'error': str(e), 'status': 'failed'}
break
if result:
results.append(result)
return results
# Example usage
def get_auth_token() -> str:
"""Fetch authentication token"""
response = requests.post('https://api.example.com/auth', json={'credentials': 'data'})
return response.json()['token']
def make_api_call(item: dict, token: str) -> dict:
"""Make API call with token"""
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'https://api.example.com/endpoint',
json=item,
headers=headers
)
response.raise_for_status()
return response.json()
if __name__ == '__main__':
items = [
{'id': 1, 'data': 'value1'},
{'id': 2, 'data': 'value2'},
{'id': 3, 'data': 'value3'}
]
results = process_items_with_retry(
items=items,
api_call_func=make_api_call,
get_token_func=get_auth_token,
max_retries=3,
retry_delay=2
)
for result in results:
if result['status'] == 'success':
print(f"Item {result['item']} processed successfully")
else:
print(f"Item {result['item']} failed: {result['error']}")
import time
import requests
from typing import List, Callable, Any, Iterator
from itertools import takewhile, count
def process_items_with_retry(
items: List[Any],
api_call_func: Callable,
get_token_func: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> List[dict]:
"""
Process items using itertools to avoid explicit loops.
Args:
items: List of items to process
api_call_func: Function that makes the API call (takes item and token)
get_token_func: Function that retrieves a fresh token
max_retries: Maximum number of retry attempts per item
retry_delay: Delay in seconds between retries
Returns:
List of results from API calls
"""
token_holder = {'token': get_token_func()}
def process_item(item: Any) -> dict:
"""Process single item with retry logic using itertools"""
result = {'item': item, 'error': 'Max retries reached', 'status': 'failed'}
for attempt in takewhile(lambda x: x < max_retries, count()):
try:
response = api_call_func(item, token_holder['token'])
return {'item': item, 'response': response, 'status': 'success'}
except requests.exceptions.HTTPError as e:
if e.response.status_code not in [401, 403]:
return {'item': item, 'error': str(e), 'status': 'failed'}
if attempt == max_retries - 1:
return {'item': item, 'error': str(e), 'status': 'failed'}
print(f"Token expired for item {item}. Refreshing token (attempt {attempt + 1}/{max_retries})")
time.sleep(retry_delay)
token_holder['token'] = get_token_func()
except Exception as e:
return {'item': item, 'error': str(e), 'status': 'failed'}
return result
return list(map(process_item, items))
# Example usage
def get_auth_token() -> str:
"""Fetch authentication token"""
response = requests.post('https://api.example.com/auth', json={'credentials': 'data'})
return response.json()['token']
def make_api_call(item: dict, token: str) -> dict:
"""Make API call with token"""
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'https://api.example.com/endpoint',
json=item,
headers=headers
)
response.raise_for_status()
return response.json()
if __name__ == '__main__':
items = [
{'id': 1, 'data': 'value1'},
{'id': 2, 'data': 'value2'},
{'id': 3, 'data': 'value3'}
]
results = process_items_with_retry(
items=items,
api_call_func=make_api_call,
get_token_func=get_auth_token,
max_retries=3,
retry_delay=2
)
for result in results:
if result['status'] == 'success':
print(f"Item {result['item']} processed successfully")
else:
print(f"Item {result['item']} failed: {result['error']}")
import time
import requests
from typing import List, Callable, Any
def process_items_with_retry(
items: List[Any],
api_call_func: Callable,
get_token_func: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> List[dict]:
"""
Process items with API calls that retry on token expiration.
Args:
items: List of items to process
api_call_func: Function that makes the API call (takes item and token)
get_token_func: Function that retrieves a fresh token
max_retries: Maximum number of retry attempts per item
retry_delay: Delay in seconds between retries
Returns:
List of results from successful API calls
"""
results = []
token = get_token_func()
for item in items:
retry_count = 0
success = False
while retry_count < max_retries and not success:
try:
# Attempt API call with current token
response = api_call_func(item, token)
results.append({
'item': item,
'response': response,
'status': 'success'
})
success = True
except requests.exceptions.HTTPError as e:
# Check if error is due to token expiration (401 or 403)
if e.response.status_code in [401, 403]:
retry_count += 1
if retry_count < max_retries:
print(f"Token expired for item {item}. Refreshing token (attempt {retry_count}/{max_retries})")
time.sleep(retry_delay)
token = get_token_func() # Get fresh token
else:
print(f"Max retries reached for item {item}")
results.append({
'item': item,
'error': str(e),
'status': 'failed'
})
else:
# Non-token error, don't retry
print(f"API error for item {item}: {e}")
results.append({
'item': item,
'error': str(e),
'status': 'failed'
})
break
except Exception as e:
print(f"Unexpected error for item {item}: {e}")
results.append({
'item': item,
'error': str(e),
'status': 'failed'
})
break
return results
# Example usage
def get_auth_token() -> str:
"""Fetch authentication token"""
# Replace with your token retrieval logic
response = requests.post('https://api.example.com/auth', json={'credentials': 'data'})
return response.json()['token']
def make_api_call(item: dict, token: str) -> dict:
"""Make API call with token"""
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'https://api.example.com/endpoint',
json=item,
headers=headers
)
response.raise_for_status()
return response.json()
if __name__ == '__main__':
# Process your items
items = [
{'id': 1, 'data': 'value1'},
{'id': 2, 'data': 'value2'},
{'id': 3, 'data': 'value3'}
]
results = process_items_with_retry(
items=items,
api_call_func=make_api_call,
get_token_func=get_auth_token,
max_retries=3,
retry_delay=2
)
# Check results
for result in results:
if result['status'] == 'success':
print(f"Item {result['item']} processed successfully")
else:
print(f"Item {result['item']} failed: {result['error']}")
import time
import requests
from typing import List, Callable, Any, Optional
def try_api_call(
item: Any,
token: str,
api_call_func: Callable,
get_token_func: Callable,
attempts_left: int,
retry_delay: int
) -> dict:
"""
Recursively attempt API call with token refresh.
Args:
item: Item to process
token: Current authentication token
api_call_func: Function that makes the API call
get_token_func: Function that retrieves a fresh token
attempts_left: Remaining retry attempts
retry_delay: Delay between retries
Returns:
Result dictionary with status and response/error
"""
if attempts_left == 0:
return {'item': item, 'error': 'Max retries reached', 'status': 'failed'}
try:
response = api_call_func(item, token)
return {'item': item, 'response': response, 'status': 'success'}
except requests.exceptions.HTTPError as e:
if e.response.status_code in [401, 403]:
print(f"Token expired for item {item}. Refreshing token ({attempts_left - 1} attempts left)")
time.sleep(retry_delay)
new_token = get_token_func()
return try_api_call(item, new_token, api_call_func, get_token_func, attempts_left - 1, retry_delay)
return {'item': item, 'error': str(e), 'status': 'failed'}
except Exception as e:
return {'item': item, 'error': str(e), 'status': 'failed'}
def process_items_with_retry(
items: List[Any],
api_call_func: Callable,
get_token_func: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> List[dict]:
"""
Process items with API calls using recursion to avoid nested loops.
Args:
items: List of items to process
api_call_func: Function that makes the API call (takes item and token)
get_token_func: Function that retrieves a fresh token
max_retries: Maximum number of retry attempts per item
retry_delay: Delay in seconds between retries
Returns:
List of results from API calls
"""
token = get_token_func()
return [
try_api_call(item, token, api_call_func, get_token_func, max_retries, retry_delay)
for item in items
]
# Example usage
def get_auth_token() -> str:
"""Fetch authentication token"""
response = requests.post('https://api.example.com/auth', json={'credentials': 'data'})
return response.json()['token']
def make_api_call(item: dict, token: str) -> dict:
"""Make API call with token"""
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'https://api.example.com/endpoint',
json=item,
headers=headers
)
response.raise_for_status()
return response.json()
if __name__ == '__main__':
items = [
{'id': 1, 'data': 'value1'},
{'id': 2, 'data': 'value2'},
{'id': 3, 'data': 'value3'}
]
results = process_items_with_retry(
items=items,
api_call_func=make_api_call,
get_token_func=get_auth_token,
max_retries=3,
retry_delay=2
)
for result in results:
if result['status'] == 'success':
print(f"Item {result['item']} processed successfully")
else:
print(f"Item {result['item']} failed: {result['error']}")
import time
import requests
from typing import List, Callable, Any, Optional
def call_with_retry(
item: Any,
token_ref: dict,
api_call_func: Callable,
get_token_func: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> dict:
"""
Make a single API call with retry logic.
Args:
item: Item to process
token_ref: Dictionary containing current token (mutable reference)
api_call_func: Function that makes the API call
get_token_func: Function that retrieves a fresh token
max_retries: Maximum retry attempts
retry_delay: Delay between retries
Returns:
Result dictionary with status and response/error
"""
for attempt in range(max_retries):
try:
response = api_call_func(item, token_ref['value'])
return {
'item': item,
'response': response,
'status': 'success'
}
except requests.exceptions.HTTPError as e:
if e.response.status_code in [401, 403] and attempt < max_retries - 1:
print(f"Token expired for item {item}. Refreshing token (attempt {attempt + 1}/{max_retries})")
time.sleep(retry_delay)
token_ref['value'] = get_token_func()
continue
return {
'item': item,
'error': str(e),
'status': 'failed'
}
except Exception as e:
return {
'item': item,
'error': str(e),
'status': 'failed'
}
return {
'item': item,
'error': 'Max retries reached',
'status': 'failed'
}
def process_items_with_retry(
items: List[Any],
api_call_func: Callable,
get_token_func: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> List[dict]:
"""
Process items with API calls that retry on token expiration.
Args:
items: List of items to process
api_call_func: Function that makes the API call (takes item and token)
get_token_func: Function that retrieves a fresh token
max_retries: Maximum number of retry attempts per item
retry_delay: Delay in seconds between retries
Returns:
List of results from API calls
"""
token_ref = {'value': get_token_func()}
results = []
for item in items:
result = call_with_retry(
item=item,
token_ref=token_ref,
api_call_func=api_call_func,
get_token_func=get_token_func,
max_retries=max_retries,
retry_delay=retry_delay
)
results.append(result)
return results
# Example usage
def get_auth_token() -> str:
"""Fetch authentication token"""
response = requests.post('https://api.example.com/auth', json={'credentials': 'data'})
return response.json()['token']
def make_api_call(item: dict, token: str) -> dict:
"""Make API call with token"""
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'https://api.example.com/endpoint',
json=item,
headers=headers
)
response.raise_for_status()
return response.json()
if __name__ == '__main__':
items = [
{'id': 1, 'data': 'value1'},
{'id': 2, 'data': 'value2'},
{'id': 3, 'data': 'value3'}
]
results = process_items_with_retry(
items=items,
api_call_func=make_api_call,
get_token_func=get_auth_token,
max_retries=3,
retry_delay=2
)
for result in results:
if result['status'] == 'success':
print(f"Item {result['item']} processed successfully")
else:
print(f"Item {result['item']} failed: {result['error']}")
from typing import List, Callable, Any
import requests
from tenacity import (
retry,
stop_after_attempt,
wait_fixed,
retry_if_exception_type,
RetryCallState
)
class TokenExpiredError(Exception):
"""Custom exception for token expiration"""
pass
class TokenManager:
"""Manages token state and refresh"""
def __init__(self, get_token_func: Callable):
self.get_token_func = get_token_func
self.token = get_token_func()
def refresh(self):
"""Refresh the token"""
print("Refreshing token...")
self.token = self.get_token_func()
def get(self) -> str:
"""Get current token"""
return self.token
def before_retry_callback(retry_state: RetryCallState):
"""Callback executed before each retry"""
token_manager = retry_state.args[1]
token_manager.refresh()
def make_retryable_call(
max_retries: int = 3,
retry_delay: int = 1
) -> Callable:
"""
Create a retryable API call function.
Args:
max_retries: Maximum retry attempts
retry_delay: Delay between retries
Returns:
Decorated function with retry logic
"""
@retry(
stop=stop_after_attempt(max_retries),
wait=wait_fixed(retry_delay),
retry=retry_if_exception_type(TokenExpiredError),
before=before_retry_callback,
reraise=True
)
def call_with_retry(item: Any, token_manager: TokenManager, api_call_func: Callable) -> dict:
"""Execute API call with automatic retry on token expiration"""
try:
response = api_call_func(item, token_manager.get())
return response
except requests.exceptions.HTTPError as e:
if e.response.status_code in [401, 403]:
raise TokenExpiredError(f"Token expired: {e}")
raise
return call_with_retry
def process_items_with_retry(
items: List[Any],
api_call_func: Callable,
get_token_func: Callable,
max_retries: int = 3,
retry_delay: int = 1
) -> List[dict]:
"""
Process items using tenacity library for retry logic.
Args:
items: List of items to process
api_call_func: Function that makes the API call (takes item and token)
get_token_func: Function that retrieves a fresh token
max_retries: Maximum number of retry attempts per item
retry_delay: Delay in seconds between retries
Returns:
List of results from API calls
"""
token_manager = TokenManager(get_token_func)
retryable_call = make_retryable_call(max_retries, retry_delay)
results = []
for item in items:
try:
response = retryable_call(item, token_manager, api_call_func)
results.append({'item': item, 'response': response, 'status': 'success'})
except Exception as e:
results.append({'item': item, 'error': str(e), 'status': 'failed'})
return results
# Example usage
def get_auth_token() -> str:
"""Fetch authentication token"""
response = requests.post('https://api.example.com/auth', json={'credentials': 'data'})
return response.json()['token']
def make_api_call(item: dict, token: str) -> dict:
"""Make API call with token"""
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'https://api.example.com/endpoint',
json=item,
headers=headers
)
response.raise_for_status()
return response.json()
if __name__ == '__main__':
items = [
{'id': 1, 'data': 'value1'},
{'id': 2, 'data': 'value2'},
{'id': 3, 'data': 'value3'}
]
results = process_items_with_retry(
items=items,
api_call_func=make_api_call,
get_token_func=get_auth_token,
max_retries=3,
retry_delay=2
)
for result in results:
if result['status'] == 'success':
print(f"Item {result['item']} processed successfully")
else:
print(f"Item {result['item']} failed: {result['error']}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment