$ pip install httpxCore sync + async client. Python 3.9+.$ pip install "httpx[http2]"Adds theh2package — required beforehttp2=Trueworks.$ pip install "httpx[cli]"A curl-like terminal client:httpx https://….import httpx★One namespace — requests-compatible surface.httpx.__version__e.g.'0.28.1'— the current stable line.$ pip install --pre httpx1.0 is in prerelease (1.0.dev5, Aug 2026); the API is largely stable but0.28.1remains the pinned default.
httpx.get(url, params=…)★Read a resource.httpx.post(url, json=…)★Create / submit.httpx.put / patch / deleteReplace · partial update · remove.httpx.head / optionsHeaders only · allowed methods / CORS.httpx.request("GET", url, **kw)Generic form; verb as a string.httpx.stream("GET", url)Streaming variant — a context manager (card 15).c.get(url) · c.post(url)Same verbs on aClientinstance — preferred (card 09).
get(url, params={"q": "x", "n": 2})★Dict →?q=x&n=2, encoded for you.params={"id": [1, 2]}List repeats the key →?id=1&id=2.params=[("k", "a"), ("k", "b")]List of tuples for ordered / duplicate keys.httpx.QueryParams({"a": 1})Immutable multidict;.merge()to add.r.url · r.url.paramsr.urlis a richhttpx.URLobject — inspect the final URL.
post(url, json={"a": 1})★JSON body +Content-Type: application/json.post(url, data={"a": 1})★Form fields →x-www-form-urlencoded. Dicts only.post(url, content=b"raw bytes")★Raw bytes / str / generator body. Usecontent=, notdata=, for raw.post(url, files={"f": open("a.png","rb")})Multipart upload (card 14 style). Mix withdata=fields.content= ≠ data=vs requestsrequests overloadeddata=for both. httpx splits them:data=is form-only.
get(url, headers={"User-Agent": "app/1.0"})★Pass request headers as a plain dict.headers={"Authorization": "Bearer <tok>"}★The usual way to send an API token (card 14).r.headers["content-type"]httpx.Headersis case-insensitive; dup values comma-joined.r.request.headersInspect exactly what was sent.c.headers.update({…})Set defaults once on a Client — sent on every call.
r.status_code★200,404… as an int.r.text★Body decoded tostr(viar.encoding).r.contentBody as rawbytes.r.json()★Parse a JSON body → dict / list.r.headers · r.cookiesResponse headers & any cookies set.r.http_version"HTTP/1.1"or"HTTP/2"— confirm h2 is live.r.url · r.elapsed · r.reason_phraseFinal URL ·timedelta· status text.r.encoding · r.num_bytes_downloadedCharset used by.text· bytes seen so far.
r.raise_for_status()★RaisesHTTPStatusErroron 4xx/5xx; returnsrso you can chain.r.is_success★Truefor 2xx. Cleaner than a range check.r.is_errorTruefor 4xx or 5xx.r.is_client_error · r.is_server_error4xx-only · 5xx-only.r.is_redirect · r.is_informational3xx with a Location · 1xx.r.status_code == httpx.codes.OKNamed codes:codes.NOT_FOUND,codes.is_error(404).# 4xx/5xx do NOT raise on their ownkeyA bad status is still a valid Response — check it.
data = r.json()Decode the response body into Python objects.post(url, json=payload)★Encode & send with the JSON Content-Type set for you.r.json() → JSONDecodeErrorgotchaRaises on a non-JSON body (HTML error page). Guard withraise_for_status()first.r.json() # uses stdlib jsonSame semantics you know; raisesjson.JSONDecodeError.
with httpx.Client() as c:★Pools & reuses connections (keep-alive); closes cleanly. The idiomatic form.Client(base_url="https://api.x.com")★Then call relative paths:c.get("/items").Client(headers=…, auth=…, cookies=…)Defaults merged into every request from this client.Client(params={"key": "<apikey>"})Query params added to all calls.Client(limits=httpx.Limits(max_connections=100))Tune the pool:max_connections,max_keepalive_connections.httpx.get() # builds+discards a clientnoteNo pooling across calls. Fine for one-offs; use a Client in a loop / service.
async with httpx.AsyncClient() as c:★Same API asClient, awaitable. Enter withasync with.r = await c.get(url)★Every request method is a coroutine —awaitit.await asyncio.gather(*[c.get(u) for u in urls])★Fire many requests concurrently on one client — the real speed win.async for chunk in r.aiter_bytes():Async streaming:aiter_bytes / aiter_text / aiter_lines.await c.aclose()Close manually if you didn't useasync with.# don't reuse a client across event loopsgotchaCreate theAsyncClientinside the running loop.
# default = 5s on every phase★"Strict timeouts everywhere" — the opposite of requests (which has none).get(url, timeout=10.0)★One float applies to all phases.httpx.Timeout(10.0, connect=5.0)Granular:connect,read,write,pool— four phases.get(url, timeout=None)dangerDisables timeouts → can hang forever. Rarely what you want.except httpx.TimeoutException:Covers Connect / Read / Write / Pool timeouts.
# follow_redirects=False by defaultvs requestsrequests follows automatically; httpx does not. The #1 surprise.get(url, follow_redirects=True)★Opt in per call, or set it once on the Client.r.historyList of redirect Responses that led here.r.next_requestTheRequestfor the next hop when not auto-following.r.has_redirect_locationTrue if the response points elsewhere.Client(max_redirects=20)Cap the chain; overflow →TooManyRedirects.
except httpx.HTTPError:★Root of the tree — catch to handle any httpx failure.except httpx.RequestError:Never got a response: network, timeout, DNS, protocol.except httpx.HTTPStatusError:★Got a response, butraise_for_status()saw 4xx/5xx.except httpx.ConnectError:Couldn't establish the connection (aNetworkError).except httpx.TimeoutException:Any of the four timeout phases.e.request · e.responseHTTPStatusErrorcarries both;RequestErrorcarriese.request.
get(url, auth=("user", "pass"))★Shorthand for HTTP Basic auth.headers={"Authorization": "Bearer <tok>"}Bearer / API tokens ride in a header, notauth=.auth=httpx.BasicAuth("u", "p")Explicit Basic; alsohttpx.DigestAuth(...).auth=httpx.NetRCAuth()Pull credentials from a.netrcfile.class MyAuth(httpx.Auth):Defineauth_flow(request)as a generator — for token refresh, signing, etc.
with httpx.stream("GET", url) as r:★Streaming is a context manager, not a kwarg.for chunk in r.iter_bytes():Alsoiter_text(),iter_lines(),iter_raw()(undecoded).async with c.stream("GET", url) as r:asyncThenasync for chunk in r.aiter_bytes():.post(url, content=byte_generator())Stream a request body up by passing a generator tocontent=.r.text # mid-stream → ResponseNotReadgotchaCallr.read()first, or stay inside the iterator.
Client(http2=True)★Enable HTTP/2 — multiplex many requests over one connection.# needs: pip install "httpx[http2]"keyWithout theh2package,http2=Truesilently falls back to 1.1.r.http_version == "HTTP/2"Confirm the negotiated protocol actually upgraded.Client(http1=False, http2=True)Force h2 only (no 1.1 fallback).
t = httpx.HTTPTransport(retries=3)Pass viaClient(transport=t).# retries = CONNECTION failures onlykeyIt does not retry on 5xx/429 status codes — usetenacityfor that.Client(mounts={"https://": t})Route different URL prefixes to different transports.httpx.ASGITransport(app=app)testCall an ASGI app (FastAPI/Starlette) in-process — no live server.httpx.WSGITransport · MockTransportTest WSGI apps, or return canned responses in tests.
r.cookies["session"]Read a cookie the server set.get(url, cookies={"session": "abc"})Send cookies with one call.with httpx.Client() as c:A Client persists cookies across calls — log in once, stay in.c.cookies.set("k", "v", domain=…)Seed the jar for later requests.jar = httpx.Cookies()A standalone cookie jar you can share.
Client(proxy="http://10.0.0.1:8080")★Singularproxy=for all traffic.proxies= # REMOVED in 0.28gotchaOldproxies=is gone → useproxy=ormounts=.Client(mounts={"https://": httpx.HTTPTransport(proxy=…)})Per-scheme / per-host proxy routing.# verify=True by default (certifi CAs)TLS validated out of the box.Client(verify=False)dangerDisables cert checks — never in production.Client(verify="/path/ca.pem", cert=…)Custom CA bundle · client-side cert (mutual TLS).