httpx & requests
requests is the classic synchronous HTTP client; httpx is its modern successor with async support, HTTP/2, and stricter defaults. Both belong in every Python developer's toolkit for APIs, webhooks, and service-to-service calls.
Search across all documentation pages
requests is the classic synchronous HTTP client; httpx is its modern successor with async support, HTTP/2, and stricter defaults. Both belong in every Python developer's toolkit for APIs, webhooks, and service-to-service calls.
import httpx
with httpx.Client(timeout=httpx.Timeout(5.0, connect=2.0)) as client:
response = client.get("https://httpbin.org/get", params={"q": "python"})
response.raise_for_status()
data = response.json()When to reach for this:
asyncio routes (httpx AsyncClient)urllib calls with readable, testable codefrom __future__ import annotations
import httpx
from pydantic import BaseModel
class User(BaseModel):
id: int
email: str
def fetch_user(user_id: int) -> User:
timeout = httpx.Timeout(10.0, connect=3.0)
with httpx.Client(base_url="https://api.example.com", timeout=timeout) as client:
response = client.get(f"/users/{user_id}")
response.raise_for_status()
return User.model_validate(response.json())
async def fetch_user_async(user_id: int) -> User:
timeout = httpx.Timeout(10.0, connect=3.0)
async with httpx.AsyncClient(base_url="https://api.example.com", timeout=timeout) as client:
response = await client.get(f"/users/{user_id}")
response.raise_for_status()
return User.model_validate(response.json())
if __name__ == "__main__":
print(fetch_user(1))What this demonstrates:
base_url avoids repeating host prefixesTimeout splits connect vs read limitsraise_for_status() turns 4xx/5xx into exceptionsClient) and async (AsyncClient)Client keeps TCP/TLS sessions warm; one-off httpx.get() creates a new pool each call.httpx.Timeout accepts connect, read, write, and pool limits; unset values fall back to the default (5s in httpx).follow_redirects=False for OAuth or signed URLs.client.stream("GET", url) yields chunks without loading the full body into memory.| Pattern | Use When |
|---|---|
with httpx.Client() | Scripts, CLI tools, sync workers |
| Module-level client (app startup) | FastAPI/Flask apps with connection reuse |
AsyncClient per request | Rare; prefer one client per app lifespan |
httpx.get() one-shot | Quick REPL probes only |
# Map transport errors to your domain layer
try:
response = client.post("/orders", json=payload)
response.raise_for_status()
except httpx.TimeoutException as exc:
raise ServiceUnavailable("upstream timeout") from exc
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 404:
raise NotFound("order missing")
raisetimeout=.Client/AsyncClient.KeyError deep in business logic. Fix: validate with Pydantic at the HTTP boundary.requests inside async routes - Blocks the event loop under load. Fix: use httpx.AsyncClient or asyncio.to_thread.response.history when debugging auth flows.Authorization) and redact in logging middleware.
| Alternative | Use When | Don't Use When |
|---|---|---|
urllib.request (stdlib) | Zero-deps scripts in restricted environments | You need timeouts, retries, or JSON ergonomics |
aiohttp | Mature async ecosystem already in the project | Team standard is httpx/FastAPI |
requests | Legacy codebases with heavy requests usage | Starting greenfield async services |
| Cloud SDK HTTP (boto3 botocore) | AWS service APIs with signing | Generic REST to third parties |
Prefer httpx for new code - same ergonomics plus async and HTTP/2. Keep requests only where migration cost exceeds benefit.
Create the client in a lifespan handler and store it on app.state, then inject via Depends in routes.
Yes by default. Pass verify=False only in local dev with self-signed certs - never in production.
with open("report.pdf", "rb") as f:
client.post("/upload", files={"file": ("report.pdf", f, "application/pdf")})Enable with httpx.Client(http2=True) when the server supports it - useful for multiplexed API gateways.
Use httpx.MockTransport or pytest-httpx to return canned responses without network I/O.
Not inside the client by default - wrap with tenacity or application-level retry logic that respects idempotency.
Pass headers={"User-Agent": "my-service/1.0"} or set client.headers on the client instance.
Yes for sync outbound calls in views or management commands. For async Django views, use AsyncClient.
Log method, URL, status, and duration - redact Authorization headers and never log full card/PII payloads.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 19, 2026