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.