mrkeyoor.com_
Wed 05 Aug 05:01 UTC
PyPIWeb Backendupdated 05 Aug 2026

httpx

HTTPX is a fully featured HTTP client for Python from the encode team (the people behind Starlette and Uvicorn). It keeps the familiar requests-style API (httpx.get, response.json, sessions-as-clients) and adds the things requests never shipped: a native async API alongside sync, HTTP/2 support, strict timeouts on by default, full type annotations, and the ability to call ASGI or WSGI apps in-process, which makes it the standard test client for FastAPI and Starlette. It sits on httpcore for transport, with certifi, idna, and anyio underneath, and includes an optional command-line client via httpx[cli]. It is the default choice for async Python HTTP.

Verdict

The best all-round Python HTTP client today and the obvious pick for async code and ASGI testing. The 0.x version number understates its stability, but the missing response-retry story and slow release cadence are real; pin your version and bring your own retries.

API stability3/5Still 0.28.x after six-plus years; the core request/response API has been steady, but minor releases have removed arguments like proxies and app with deprecation windows, which is breaking-change territory for pinned-loose projects.
Docs5/5python-httpx.org is a model project site: quickstart, async guide, transports, HTTP/2, and a requests-compatibility page that lists exact behavioral differences, plus a complete typed API reference.
Maintenance3/5The repo saw pushes as recently as March 2026 and issues are triaged (143 open), but the last PyPI release is 0.28.1 from December 2024, so fixes sit unreleased and the cadence has clearly slowed.
Ecosystem4/5Roughly 199M weekly downloads, the blessed test client for FastAPI/Starlette, and a documented third-party ecosystem (auth plugins, mocking via respx); it trails requests only in sheer legacy integration surface.

Use it if

  • You are writing async Python (FastAPI, asyncio services) and need an HTTP client with a real async API instead of running requests in threads
  • You are testing an ASGI app; ASGITransport calls FastAPI or Starlette apps in-process with no server, which is exactly how their test docs tell you to do it
  • You want requests-like ergonomics but with type annotations, HTTP/2, and timeouts that fail fast by default instead of hanging forever
  • You need one client library that does both sync and async with the same API so shared code paths do not fork
Skip it if

Setup reality

pip install httpx pulls a small pure-Python dependency set (httpcore, anyio, certifi, idna), no compilation. HTTP/2 is not included by default, you need pip install httpx[http2] and then http2=True on the client, which people forget and then wonder why everything is HTTP/1.1. The 5-second default timeout is the biggest migration surprise from requests: slow endpoints that used to hang now raise, which is good design but breaks naive scripts. Version upgrades within 0.x have removed keyword arguments (0.26 dropped app=, 0.28 dropped proxies= in favor of proxy= and mounts=), so pin the version and read the changelog before bumping.

Patterns

One-off GET requestsimple-get

import httpx

r = httpx.get('https://api.example.com/items', params={'page': 2})
r.raise_for_status()
data = r.json()

Top-level httpx.get opens and closes a connection each call; anything beyond a couple of requests should use a Client.

Reuse connections with a Clientclient-reuse

import httpx

with httpx.Client(base_url='https://api.example.com',
                  headers={'Authorization': f'Bearer {token}'}) as client:
    a = client.get('/users/1')
    b = client.get('/users/2')

The Client keeps the connection pool alive (like requests.Session); create one per app, not per request.

Async requests with AsyncClientasync-requests

import asyncio
import httpx

async def main():
    async with httpx.AsyncClient() as client:
        results = await asyncio.gather(
            client.get('https://api.example.com/a'),
            client.get('https://api.example.com/b'),
        )
    return [r.json() for r in results]

asyncio.run(main())

Sync and async clients share the same method names; just do not share one AsyncClient across event loops.

POST a JSON bodypost-json

r = client.post('https://api.example.com/items',
                json={'name': 'widget', 'qty': 3})
r.raise_for_status()

Use json= for JSON, data= for form-encoded bodies; passing a dict to data= silently sends a form, not JSON.

Configure timeouts explicitlytimeouts

timeout = httpx.Timeout(10.0, connect=5.0)
client = httpx.Client(timeout=timeout)

# or disable for a long streaming call
client.get(url, timeout=None)

Everything defaults to 5 seconds, unlike requests which waits forever; long downloads or slow APIs need this raised or you get httpx.ReadTimeout.

Handle errors by typeerror-handling

try:
    r = client.get(url)
    r.raise_for_status()
except httpx.TimeoutException:
    ...  # connect/read/write/pool timeout
except httpx.HTTPStatusError as e:
    print(e.response.status_code, e.response.text)
except httpx.RequestError as e:
    print(f'network problem talking to {e.request.url}')

4xx/5xx responses do not raise on their own; nothing happens until you call raise_for_status().

Stream a large download to diskstreaming-download

with client.stream('GET', 'https://example.com/big.iso') as r:
    r.raise_for_status()
    with open('big.iso', 'wb') as f:
        for chunk in r.iter_bytes(chunk_size=65536):
            f.write(chunk)

Accessing r.text or r.json() inside a stream block raises unless you call r.read() first; the body is not loaded by default.

Retry failed connections at the transportconnect-retries

transport = httpx.HTTPTransport(retries=3)
client = httpx.Client(transport=transport)

This retries connection errors only, never 5xx responses; for response-level retries with backoff use a loop or a library like tenacity.

Enable HTTP/2http2

# pip install 'httpx[http2]'
client = httpx.Client(http2=True)
r = client.get('https://www.example.org')
print(r.http_version)  # 'HTTP/2'

Without the [http2] extra installed, http2=True raises at import of the h2 machinery; it also only applies to Client, not top-level httpx.get.

Route through a proxyproxy

client = httpx.Client(proxy='http://localhost:8030')

# per-scheme routing uses mounts
client = httpx.Client(mounts={
    'http://': httpx.HTTPTransport(proxy='http://localhost:8030'),
    'https://': httpx.HTTPTransport(proxy='http://localhost:8031'),
})

The old proxies= argument was removed in 0.28; code copied from pre-2024 answers fails with a TypeError.

Test a FastAPI/Starlette app in-processtest-asgi-app

import httpx
from myproject.main import app

transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport,
                             base_url='http://test') as client:
    r = await client.get('/health')
    assert r.status_code == 200

The app= shortcut on the client was removed; you must wrap the app in ASGITransport yourself now.

Authentication on every requestauth

# basic auth
client = httpx.Client(auth=('user', 'pass'))

# custom scheme via a reusable Auth class
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(token))

httpx.Auth subclasses can react to responses (e.g. refresh on 401) by yielding more requests from auth_flow.

Alternatives

PackageRegistryPick it when
requestsPyPISynchronous-only code where ubiquity and unchanging APIs matter more than async or HTTP/2
aiohttpPyPIHigh-concurrency async workloads where raw throughput beats API ergonomics, or you also need the server side
urllib3PyPILow-level control over pooling and retries with a built-in Retry class, no session sugar needed