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 httpx

Basic 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.

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/users

Timeouts

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

Featurerequestshttpx
Sync
Async
HTTP/2
Type hintsPartialFull
Timeout defaultNone5s

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: