| name | requests |
| description | [Applies to: **/*.py] This guide outlines definitive best practices for using the `requests` library in Python, focusing on performance, reliability, and maintainability for API clients and web interactions. |
| source | cursor_mdc |
requests Best Practices
requests is the definitive HTTP client for Python. To build robust, performant, and maintainable web interactions, you must leverage its advanced features and integrate them with modern Python best practices. This guide provides actionable rules for our team.
1. Always Use Session Objects
For any code making more than a single HTTP call, or any reusable API client, you must use a requests.Session object. Sessions provide connection pooling and cookie persistence, drastically improving performance and resource usage.
❌ BAD: Direct requests calls
import requests
response1 = requests.get("https://api.example.com/data/1")
response2 = requests.get("https://api.example.com/data/2")
✅ GOOD: Use a Session with a context manager
import requests
with requests.Session() as session:
response1 = session.get("https://api.example.com/data/1")
response2 = session.get("https://api.example.com/data/2")
2. Configure Retries and Timeouts with HTTPAdapter
Enhance session reliability by mounting a custom HTTPAdapter to handle retries with backoff and set default timeouts. This prevents flaky network issues from crashing your application and ensures requests don't hang indefinitely.
❌ BAD: No retries, no default timeouts
import requests
with requests.Session() as session:
response = session.get("https://api.example.com/flaky-endpoint")
✅ GOOD: Mount an HTTPAdapter with Retry logic
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
with requests.Session() as session:
session.mount("http://", adapter)
session.mount("https://", adapter)
response = session.get("https://api.example.com/flaky-endpoint", timeout=(5, 10))
Note: timeout should still be explicitly set on individual requests, even with an adapter, to override or confirm the default.
3. Always Set Explicit Timeouts
Never make a requests call without an explicit timeout parameter. This prevents your application from hanging indefinitely due to slow or unresponsive servers. Specify a tuple (connect_timeout, read_timeout).
connect_timeout: The time limit for the client to establish a connection to the server.
read_timeout: The time limit for the client to wait for a response after sending the request.
❌ BAD: Missing timeout
response = session.get("https://api.example.com/slow-endpoint")
✅ GOOD: Explicit (connect, read) timeout
response = session.get("https://api.example.com/slow-endpoint", timeout=(5, 10))
4. Handle Responses Robustly
Process responses defensively. Always check for HTTP status codes, handle JSON parsing errors, and log sufficient context for debugging.
❌ BAD: Naive response handling
response = session.get("https://api.example.com/data")
data = response.json()
print(data)
✅ GOOD: raise_for_status() and try/except for JSON
import logging
import requests
logging.basicConfig(level=logging.INFO)
try:
response = session.get("https://api.example.com/data", timeout=(5, 10))
response.raise_for_status()
try:
data = response.json()
logging.info(f"Successfully fetched data from {response.url}")
except requests.exceptions.JSONDecodeError as e:
logging.error(f"Failed to decode JSON from {response.url}: {e}")
logging.error(f"Response content: {response.text[:200]}")
raise
except requests.exceptions.HTTPError as e:
logging.error(f"HTTP Error for {e.request.url}: {e.response.status_code} - {e.response.text}")
raise
except requests.exceptions.ConnectionError as e:
logging.error(f"Connection Error for {e.request.url}: {e}")
raise
except requests.exceptions.Timeout as e:
logging.error(f"Timeout Error for {e.request.url}: {e}")
raise
except requests.exceptions.RequestException as e:
logging.error(f"An unexpected error occurred during request to : ")
5. Encapsulate Client Logic
Isolate all requests logic within a dedicated API client class or module. This improves testability, reusability, and maintainability. Use type hints for clarity.
❌ BAD: Scattered requests calls
def process_user_data(user_id):
resp = requests.get(f"https://api.example.com/users/{user_id}")
def fetch_product_info(product_id):
resp = requests.get(f"https://api.example.com/products/{product_id}")
✅ GOOD: Dedicated API Client Class
import requests
import logging
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from typing import Any, Dict, Optional, Tuple
logger = logging.getLogger(__name__)
class ExampleAPIClient:
"""
Client for interacting with the Example API.
"""
def __init__(
self,
base_url: str,
api_key: str,
session: Optional[requests.Session] = None,
timeout: Tuple[float, float] = (5, 10)
):
self.base_url = base_url
self.api_key = api_key
self.timeout = timeout
if session is None:
self.session = requests.Session()
self._configure_session()
else:
self.session = session
def _configure_session(self) -> None:
"""Configures the session with adapters and default headers."""
retry_strategy = Retry(
total=3, backoff_factor=1, status_forcelist=[, , , , ]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
.session.mount(, adapter)
.session.mount(, adapter)
.session.headers.update({: })
.session.headers.update({: })
() -> [, ]:
url =
:
response = .session.request(
method, url, params=params, json=json, data=data, headers=headers, timeout=.timeout
)
response.raise_for_status()
response.json()
requests.exceptions.JSONDecodeError e:
logger.error()
requests.exceptions.RequestException e:
logger.error()
(e, ) e.response :
logger.error()
() -> [, ]:
._request(, )
() -> [, ]:
._request(, , json=user_data)
api_client ExampleAPIClient
os
API_BASE_URL = os.getenv(, )
API_KEY = os.getenv(, )
:
client = ExampleAPIClient(base_url=API_BASE_URL, api_key=API_KEY)
user = client.get_user()
()
new_user = client.create_user({: , : })
()
requests.exceptions.RequestException e:
()
6. Avoid Hardcoding URLs and Credentials
Externalize base URLs, API keys, and other sensitive configurations using environment variables or dedicated configuration files. This is crucial for security and deploying across different environments (dev, staging, prod).
❌ BAD: Hardcoded values
API_URL = "https://prod.api.example.com/v1"
AUTH_TOKEN = "super_secret_token_123"
✅ GOOD: Environment variables
import os
API_URL = os.getenv("EXAMPLE_API_URL", "https://dev.api.example.com/v1")
AUTH_TOKEN = os.getenv("EXAMPLE_API_TOKEN")
if not AUTH_TOKEN:
raise ValueError("EXAMPLE_API_TOKEN environment variable not set.")
7. Test Your Client Code
When testing your API client, mock the requests library to avoid making actual network calls. This makes tests fast, reliable, and independent of external service availability. Libraries like responses or unittest.mock are excellent for this.
import unittest
from unittest.mock import patch, MagicMock
from api_client import ExampleAPIClient
import requests
class TestExampleAPIClient(unittest.TestCase):
def setUp(self):
self.mock_session = MagicMock(spec=requests.Session)
self.client = ExampleAPIClient(
base_url="http://test.api.com", api_key="test_key", session=self.mock_session
)
def test_get_user_success(self):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": "123", "name": "Test User"}
mock_response.raise_for_status.return_value = None
self.mock_session.request.return_value = mock_response
user = self.client.get_user("123")
self.assertEqual(user["name"], "Test User")
self.mock_session.request.assert_called_once_with(
"GET", "http://test.api.com/users/123",
params=None, json=None, data=None, headers=None, timeout=(5, 10)
)
def ():
.mock_session.request.side_effect = requests.exceptions.HTTPError(
)
.assertRaises(requests.exceptions.HTTPError):
.client.get_user()
__name__ == :
unittest.main()