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.
Recipe
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:
- Calling REST/JSON APIs from scripts, workers, or FastAPI background tasks
- Need async HTTP in
asyncioroutes (httpxAsyncClient) - Uploading files or streaming large responses
- Replacing ad-hoc
urllibcalls with readable, testable code
Working Example
from __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_urlavoids repeating host prefixes- Explicit
Timeoutsplits connect vs read limits raise_for_status()turns 4xx/5xx into exceptions- Pydantic validates JSON payloads at the boundary
- Same pattern works sync (
Client) and async (AsyncClient)
Deep Dive
How It Works
- Connection pooling - Reusing a
Clientkeeps TCP/TLS sessions warm; one-offhttpx.get()creates a new pool each call. - Timeouts -
httpx.Timeoutaccepts connect, read, write, and pool limits; unset values fall back to the default (5s in httpx). - Redirects - Followed by default; disable with
follow_redirects=Falsefor OAuth or signed URLs. - Streaming -
client.stream("GET", url)yields chunks without loading the full body into memory.
Client Lifecycle
| 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 |
Python Notes
# 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")
raiseGotchas
- No timeout set - Infinite hangs stall workers and obscure outages. Fix: always pass
timeout=. - Creating a new client per request - Destroys pooling and slows TLS handshakes. Fix: reuse
Client/AsyncClient. - Trusting JSON without validation - Schema drift causes
KeyErrordeep in business logic. Fix: validate with Pydantic at the HTTP boundary. - Blocking
requestsinside async routes - Blocks the event loop under load. Fix: usehttpx.AsyncClientorasyncio.to_thread. - Ignoring redirects on POST - Some APIs return 302 to a different host. Fix: log
response.historywhen debugging auth flows. - Hard-coded secrets in URLs - Query tokens end up in logs. Fix: use headers (
Authorization) and redact in logging middleware.
Alternatives
| 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 |
FAQs
Should new projects use httpx or requests?
Prefer httpx for new code - same ergonomics plus async and HTTP/2. Keep requests only where migration cost exceeds benefit.
How do I share one AsyncClient across FastAPI routes?
Create the client in a lifespan handler and store it on app.state, then inject via Depends in routes.
Does httpx verify TLS certificates?
Yes by default. Pass verify=False only in local dev with self-signed certs - never in production.
How do I send multipart file uploads?
with open("report.pdf", "rb") as f:
client.post("/upload", files={"file": ("report.pdf", f, "application/pdf")})What about HTTP/2?
Enable with httpx.Client(http2=True) when the server supports it - useful for multiplexed API gateways.
How do I mock HTTP in tests?
Use httpx.MockTransport or pytest-httpx to return canned responses without network I/O.
Should I retry failed requests automatically?
Not inside the client by default - wrap with tenacity or application-level retry logic that respects idempotency.
How do I set a custom User-Agent?
Pass headers={"User-Agent": "my-service/1.0"} or set client.headers on the client instance.
Can I use httpx with Django?
Yes for sync outbound calls in views or management commands. For async Django views, use AsyncClient.
How do I log request/response bodies safely?
Log method, URL, status, and duration - redact Authorization headers and never log full card/PII payloads.
Related
- tenacity - retries and backoff for flaky upstreams
- pydantic-settings - typed config for API base URLs
- FastAPI Basics - async routes that call external APIs
- beautifulsoup4 & lxml - parsing HTML fetched over HTTP
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+.