HTTP Clients and API Calls
What You'll Learn
How to make HTTP requests in Python, handle API responses, deal with errors and timeouts, and write reliable API client code.
The requests Library
pip install requests
import requests
# GET request
response = requests.get("https://api.github.com/repos/python/cpython")
print(response.status_code) # 200
print(response.headers["Content-Type"]) # application/json
data = response.json() # parse JSON body
print(data["stargazers_count"])
Always Set Timeouts
Without a timeout, your code can hang indefinitely:
# ❌ No timeout — can hang forever
response = requests.get("https://api.example.com/data")
# ✅ Always set timeout
response = requests.get(
"https://api.example.com/data",
timeout=(5, 30) # (connect_timeout, read_timeout) in seconds
)
Handling Response Errors
import requests
from requests.exceptions import HTTPError, ConnectionError, Timeout
def fetch_user(user_id: int) -> dict:
try:
response = requests.get(
f"https://api.example.com/users/{user_id}",
timeout=10,
)
response.raise_for_status() # raises HTTPError for 4xx/5xx
return response.json()
except HTTPError as e:
if e.response.status_code == 404:
raise ValueError(f"User {user_id} not found")
elif e.response.status_code == 429:
raise RuntimeError("Rate limit exceeded")
raise RuntimeError(f"API error {e.response.status_code}: {e}")
except ConnectionError:
raise RuntimeError("Cannot connect to API — check network")
except Timeout:
raise RuntimeError("API request timed out")
Making Different Request Types
import requests
BASE_URL = "https://api.example.com"
# GET — fetch data
response = requests.get(f"{BASE_URL}/users/42")
# POST — create resource
response = requests.post(
f"{BASE_URL}/users",
)
# PUT — replace resource
response = requests.put(
f"{BASE_URL}/users/42",
)
# PATCH — partial update
response = requests.patch(
f"{BASE_URL}/users/42",
)
# DELETE — remove resource
response = requests.delete(f"{BASE_URL}/users/42")
Query Parameters
# ?page=2&per_page=50&status=active
response = requests.get(
"https://api.example.com/users",
params={
"page": 2,
"per_page": 50,
"status": "active",
}
)
print(response.url)
# https://api.example.com/users?page=2&per_page=50&status=active
Sessions — Reuse Connections
import requests
# Session reuses TCP connections and applies headers to all requests
session = requests.Session()
session.headers.update({
"User-Agent": "MyApp/1.0",
"Accept": "application/json",
})
# All requests through session use the headers + connection pooling
r1 = session.get("https://api.example.com/users")
r2 = session.get("https://api.example.com/products")
session.close()
# Better: use as context manager (auto-closes)
with requests.Session() as session:
session.headers.update({"User-Agent": "MyApp/1.0"})
r = session.get("https://api.example.com/users")
Building a Simple API Client Class
import requests
import logging
from typing import Any
log = logging.getLogger(__name__)
class APIClient:
"""Simple REST API client with error handling and logging."""
def __init__(self, base_url: str, api_key: str, timeout: int = 30):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
"Content-Type": "application/json",
})
def _request(self, method: str, path: str, **kwargs) -> Any:
url = f"{self.base_url}/{path.lstrip('/')}"
log.debug("%s %s", method.upper(), url)
response = self.session.request(
method, url, timeout=self.timeout, **kwargs
)
response.raise_for_status()
return response.json() if response.content else None
def get(self, path: str, params: dict = None) -> Any:
return self._request("GET", path, params=params)
def post(self, path: str, data: dict) -> Any:
return self._request("POST", path, json=data)
def patch(self, path: str, data: dict) -> Any:
return self._request("PATCH", path, json=data)
def delete(self, path: str) -> None:
self._request("DELETE", path)
def close(self):
self.session.close()
def __enter__(self):
return self
def __exit__(self, *_):
self.close()
# Usage
with APIClient("https://api.example.com", api_key="secret") as client:
user = client.get("/users/42")
Inspecting Responses
response = requests.get("https://api.example.com/data", timeout=10)
# Status
print(response.status_code) # 200
print(response.ok) # True if 200-299
print(response.reason) # "OK"
# Headers
print(response.headers["Content-Type"])
print(response.headers.get("X-Rate-Limit-Remaining"))
# Body
print(response.text) # raw string
print(response.json()) # parsed JSON
print(response.content) # raw bytes
# Request details (for debugging)
print(response.request.url)
print(response.request.headers)
print(response.elapsed) # timedelta of request duration
Common Mistakes
| Mistake | Fix |
|---|---|
No timeout= | Always set timeout=(5, 30) |
Not calling raise_for_status() | Check response status |
| Creating new Session per request | Reuse Session with connection pooling |
| Logging full response body | Log status code and size only |
| Hardcoding API keys | Read from environment variables |
Quick Reference
import requests
# GET
r = requests.get(url, params={"key": "val"}, timeout=10)
r.raise_for_status()
data = r.json()
# POST
r = requests.post(url, json={"key": "val"}, timeout=10)
# With auth
r = requests.get(url, headers={"Authorization": f"Bearer {token}"})
# Session
with requests.Session() as s:
s.headers.update({"Authorization": f"Bearer {token}"})
r = s.get(url, timeout=10)
# Error handling
from requests.exceptions import HTTPError, Timeout, ConnectionError
try:
r = requests.get(url, timeout=10)
r.raise_for_status()
except HTTPError as e:
status = e.response.status_code
except Timeout:
...
except ConnectionError:
...