httpx vs requests: the default differences that break ported code (tested)
httpx copies the requests API but not its defaults: it does not follow redirects, times out after 5 s (requests never does), raises on 3xx in raise_for_status(), and uses different exception names. It adds AsyncClient and optional HTTP/2.
httpx looks like requests (httpx.get(url, params=..., json=...), r.json(), r.raise_for_status()), but several defaults differ, and they are what breaks code ported from requests. Measured with httpx 0.28.1 and requests 2.34.2 on Python 3.13.5 against a local http.server (/r302 redirects to /final). No speed benchmark here: for a single request the network dominates either way.
1. Redirects and timeouts: opposite defaults
import inspect, httpx, requests
from srv import start
base = start()
for lib in (requests, httpx):
r = lib.get(base + "/r302")
print(f"{lib.__name__:8} {lib.__version__:7} status={r.status_code} url={r.url}")
print("requests timeout default:", inspect.signature(requests.adapters.HTTPAdapter.send).parameters["timeout"].default)
print("httpx timeout default: ", httpx.Client().timeout)
Output:
requests 2.34.2 status=200 url=http://127.0.0.1:8963/final
httpx 0.28.1 status=302 url=http://127.0.0.1:8963/r302
requests timeout default: None
httpx timeout default: Timeout(timeout=5.0)
requests follows redirects and has no timeout (a stalled server hangs forever); httpx does not follow redirects and times out after 5 seconds. Port code with follow_redirects=True and an explicit timeout=. Details: httpx follow redirects, httpx timeout default.
2. raise_for_status() also raises on 3xx in httpx
import httpx, requests
from srv import start
base = start()
for lib, r in (("requests", requests.get(base + "/r302", allow_redirects=False)),
("httpx", httpx.get(base + "/r302"))):
try:
r.raise_for_status()
print(f"{lib:8} {r.status_code}: raise_for_status() passes")
except Exception as e:
print(f"{lib:8} {r.status_code}: raise_for_status() -> {type(e).__name__}")
Output:
requests 302: raise_for_status() passes
httpx 302: raise_for_status() -> HTTPStatusError
requests raises only for 4xx/5xx; httpx raises HTTPStatusError for anything that is not 2xx, including an unfollowed redirect.
3. Different exception and argument names
import httpx, requests
for name, call in (("requests", lambda: requests.get("http://127.0.0.1:8999/")),
("httpx", lambda: httpx.get("http://127.0.0.1:8999/"))):
try:
call()
except Exception as e:
print(f"{name:8} {type(e).__module__}.{type(e).__name__}")
try:
httpx.get("http://127.0.0.1:8999/", allow_redirects=True)
except TypeError as e:
print("TypeError:", e)
Output:
requests requests.exceptions.ConnectionError
httpx httpx.ConnectError
TypeError: get() got an unexpected keyword argument 'allow_redirects'. Did you mean 'follow_redirects'?
Connection refused is requests.exceptions.ConnectionError vs httpx.ConnectError (timeouts: requests.exceptions.Timeout vs httpx.TimeoutException), so except clauses must change. allow_redirects= is a TypeError in httpx. TLS failures are also a ConnectError in httpx: see httpx CERTIFICATE_VERIFY_FAILED.
4. What httpx adds: async and HTTP/2
import httpx
try:
httpx.Client(http2=True)
except ImportError as e:
print("ImportError:", e)
Output:
ImportError: Using http2=True, but the 'h2' package is not installed. Make sure to install httpx using `pip install httpx[http2]`.
httpx has httpx.AsyncClient with the same API for asyncio code (requests has no async support), and optional HTTP/2, which needs the extra pip install "httpx[http2]". If you need neither, requests is fine; if you use async code, httpx (or aiohttp) avoids blocking the event loop.
All httpx guides: params, headers, post json vs data, auth, follow redirects, timeout, SSL verify, proxy, retries, RemoteProtocolError, streaming, AsyncClient connection pool, raise_for_status, cookies, file upload, file download, MockTransport for tests, logging, HTTP/2, base_url, verify=False not working, JSON decode error, websockets, Event loop is closed.
Related: requests ReadTimeout, requests ConnectionError max retries, requests HTTPError response message.
Frequently asked questions
Is httpx a drop-in replacement for requests?
Not quite. In the test httpx returned a 302 where requests followed the redirect, had a 5 s default timeout vs none, and used different exception and argument names.
Does requests have a default timeout?
No. HTTPAdapter.send defaults to timeout=None in requests 2.34.2, so always pass timeout=.
When should I use httpx instead of requests?
When you need async (AsyncClient) or HTTP/2. For simple synchronous scripts both work.
Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 and requests 2.34.2 on Python 3.13.5 against a local http.server. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx and requests are free.