requests is great, but httpx is better. Here's why and how to use it.
Why httpx?
- Sync and async with the same API
- HTTP/2 support
- Better timeout handling
- Type hints throughout
- Actively maintained
Installation
pip install httpxBasic Usage
import httpx
# GET request
response = httpx.get("https://api.example.com/users")
print(response.status_code)
print(response.json())
# POST with JSON
response = httpx.post(
"https://api.example.com/users",
json={"name": "Owen", "email": "owen@example.com"}
)Looks just like requests. The API is intentionally similar.
Using a Client (Recommended)
For multiple requests, use a client:
import httpx
with httpx.Client() as client:
users = client.get("/users").json()
posts = client.get("/posts").json()Benefits:
- Connection pooling
- Shared configuration
- Better performance
Base URL and Headers
client = httpx.Client(
base_url="https://api.example.com",
headers={"Authorization": "Bearer token123"}
)
# Now requests are simpler
users = client.get("/users").json() # Full URL: https://api.example.com/usersTimeouts
Always set timeouts:
# Timeout for all operations
client = httpx.Client(timeout=10.0)
# Granular timeouts
client = httpx.Client(
timeout=httpx.Timeout(
connect=5.0, # Connection timeout
read=10.0, # Read timeout
write=10.0, # Write timeout
pool=5.0 # Pool timeout
)
)
# Per-request override
response = client.get("/slow-endpoint", timeout=30.0)Default is 5 seconds. Explicit is better.
Error Handling
import httpx
try:
response = httpx.get("https://api.example.com/users")
response.raise_for_status() # Raises for 4xx/5xx
data = response.json()
except httpx.ConnectError:
print("Failed to connect")
except httpx.TimeoutException:
print("Request timed out")
except httpx.HTTPStatusError as e:
print(f"HTTP error: {e.response.status_code}")Async Usage
Same API, just async:
import httpx
import asyncio
async def fetch_users():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/users")
return response.json()
users = asyncio.run(fetch_users())Parallel Requests
import httpx
import asyncio
async def fetch_all():
async with httpx.AsyncClient() as client:
tasks = [
client.get("https://api.example.com/users"),
client.get("https://api.example.com/posts"),
client.get("https://api.example.com/comments"),
]
responses = await asyncio.gather(*tasks)
return [r.json() for r in responses]
results = asyncio.run(fetch_all())Three requests in parallel instead of sequential.
Retries
httpx doesn't have built-in retries, but it's easy to add:
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10)
)
def fetch_with_retry(url):
response = httpx.get(url)
response.raise_for_status()
return response.json()Or use httpx with a transport:
import httpx
from urllib3.util.retry import Retry
# Custom transport with retries
transport = httpx.HTTPTransport(retries=3)
client = httpx.Client(transport=transport)Query Parameters
# Simple
response = client.get("/search", params={"q": "python", "limit": 10})
# Multiple values
response = client.get("/search", params={"tag": ["python", "http"]})File Uploads
# Single file
files = {"file": open("document.pdf", "rb")}
response = client.post("/upload", files=files)
# With filename and content type
files = {
"file": ("report.pdf", open("document.pdf", "rb"), "application/pdf")
}
response = client.post("/upload", files=files)Streaming Responses
For large responses:
with client.stream("GET", "/large-file") as response:
for chunk in response.iter_bytes():
process(chunk)Authentication
# Basic auth
client = httpx.Client(auth=("username", "password"))
# Bearer token
client = httpx.Client(
headers={"Authorization": "Bearer token123"}
)
# Custom auth
class TokenAuth(httpx.Auth):
def __init__(self, token):
self.token = token
def auth_flow(self, request):
request.headers["Authorization"] = f"Bearer {self.token}"
yield request
client = httpx.Client(auth=TokenAuth("my-token"))httpx vs requests
| Feature | requests | httpx |
|---|---|---|
| Sync | ✓ | ✓ |
| Async | ✗ | ✓ |
| HTTP/2 | ✗ | ✓ |
| Type hints | Partial | Full |
| Timeout default | None | 5s |
Use httpx for new projects. It's the modern choice.
My Patterns
import httpx
from contextlib import contextmanager
@contextmanager
def api_client():
"""Configured client for our API."""
client = httpx.Client(
base_url="https://api.example.com",
timeout=10.0,
headers={"User-Agent": "MyApp/1.0"}
)
try:
yield client
finally:
client.close()
# Usage
with api_client() as client:
users = client.get("/users").json()Keep client configuration in one place. Reuse everywhere.
React to this post: