Skip to content

HTTPX — Fundamentals

Client vs Module-Level Functions

import httpx

# module-level: new connection per request (fine for one-off scripts)
r = httpx.get("https://api.example.com/users", timeout=10)

# Client: reuses connections, shares config (use for multiple requests)
with httpx.Client(base_url="https://api.example.com", timeout=10) as client:
    r = client.get("/users")

Always prefer Client for production code — connection pooling, shared headers, cookies.


GET — Read Data

import httpx

with httpx.Client(timeout=10) as client:
    # basic GET
    r = client.get("https://api.example.com/users")
    r.raise_for_status()
    users = r.json()

    # query parameters — encoded automatically
    r = client.get("https://api.example.com/users", params={
        "page": 2,
        "per_page": 25,
        "role": "admin",
    })

    # repeated keys
    r = client.get(url, params=[("tag", "python"), ("tag", "http")])

POST — Create Data

# JSON body (most common) — auto-sets Content-Type
r = client.post("/users", json={"name": "Alice", "email": "alice@example.com"})

# form data — application/x-www-form-urlencoded
r = client.post("/login", data={"username": "alice", "password": "secret"})

# raw binary
r = client.post("/upload", content=b"raw bytes", headers={"Content-Type": "application/octet-stream"})

Note: httpx uses content= for raw bytes (not data=b"..." like requests).


PUT / PATCH / DELETE

# PUT replaces the entire resource
r = client.put(f"/users/{user_id}", json={"name": "Updated", "email": "a@test.com"})

# PATCH updates only provided fields
r = client.patch(f"/users/{user_id}", json={"role": "admin"})

# DELETE removes the resource
r = client.delete(f"/users/{user_id}")
r.raise_for_status()

Headers

# set on client — shared across all requests
with httpx.Client(headers={
    "Authorization": f"Bearer {token}",
    "Accept": "application/json",
}) as client:
    r = client.get("/me")

# per-request override (merges with client headers)
r = client.get("/admin", headers={"X-Request-ID": "abc-123"})

# read response headers (case-insensitive)
content_type = r.headers["content-type"]
rate_limit = r.headers.get("x-ratelimit-remaining")

Cookies

# send cookies explicitly
r = client.get(url, cookies={"session_id": "abc123"})

# read from response
session = r.cookies.get("session_id")

# Client auto-persists cookies across requests (like a browser)
with httpx.Client() as client:
    client.get("https://example.com/login")   # sets cookie
    client.get("https://example.com/profile")  # cookie sent automatically

Response Object

r = client.get("/users/1")

r.status_code         # 200
r.is_success          # True (2xx) — httpx-specific
r.is_client_error     # True (4xx)
r.is_server_error     # True (5xx)
r.json()              # parsed JSON body
r.text                # decoded string
r.content             # raw bytes
r.headers             # case-insensitive headers
r.url                 # final URL after redirects
r.elapsed             # round-trip timedelta
r.http_version        # "HTTP/1.1" or "HTTP/2"
r.raise_for_status()  # raises HTTPStatusError on 4xx/5xx

Note: httpx does not follow redirects by default — use follow_redirects=True on client or per-request.


Error Handling

Exception Trigger
httpx.HTTPError Base class for all errors
httpx.RequestError Network failure (DNS, connection, timeout)
httpx.TimeoutException Any timeout exceeded
httpx.ConnectTimeout Connect phase timeout
httpx.ReadTimeout Read phase timeout
httpx.WriteTimeout Write phase timeout
httpx.PoolTimeout No connection available in pool
httpx.HTTPStatusError Raised by raise_for_status() on 4xx/5xx

Robust Pattern

import logging

import httpx

log = logging.getLogger(__name__)


def fetch_user(user_id: str, client: httpx.Client) -> dict:
    url = f"/users/{user_id}"
    try:
        r = client.get(url)
        r.raise_for_status()
        return r.json()
    except httpx.TimeoutException:
        log.error("Timeout: %s", url)
        raise
    except httpx.RequestError as exc:
        # DNS failure, connection refused, network unreachable
        log.error("Request failed: %s%s", url, exc)
        raise
    except httpx.HTTPStatusError as exc:
        log.error("HTTP %s: %s", exc.response.status_code, url)
        raise

File Uploads

with open("report.pdf", "rb") as f:
    r = client.post("/upload", files={"file": ("report.pdf", f, "application/pdf")})

with open("photo.jpg", "rb") as f:  # file + form fields
    r = client.post("/upload", data={"title": "My Photo"}, files={"file": f})

Key Differences from requests

Feature requests httpx
Async No AsyncClient
HTTP/2 No http2=True
Redirects Auto-follow Explicit opt-in
Raw body data=b"..." content=b"..."
Timeout default None 5 seconds
Type hints Partial Full