The architecture of HTTPX, x-rayed
HTTPX enforces its public API with a naming convention you can see straight in the x-ray: every single module is underscore-prefixed (_client.py, _models.py, _transports/). The entire supported surface is whatever __init__.py chooses to re-export — 23 private components behind one public door.
archsteer xray v0.4.1 against encode/httpx@b5addb6 (httpx/) on 2026-07-05. Read-only static analysis — no code executed, no architecture rules declared. Structure, not judgment.Module dependency graph
Top 14 modules by connectivity; an arrow means "imports from".
graph LR
m0["__init__"]
m1["_api"]
m2["_auth"]
m3["_client"]
m4["_config"]
m5["_content"]
m6["_exceptions"]
m7["_main"]
m8["_models"]
m9["_multipart"]
m10["_transports"]
m11["_types"]
m12["_urls"]
m13["_utils"]
m0 --> m1
m0 --> m2
m0 --> m3
m0 --> m4
m0 --> m5
m0 --> m6
m0 --> m7
m0 --> m8
m0 --> m10
m0 --> m11
m0 --> m12
m1 --> m3
m1 --> m4
m1 --> m8
m1 --> m11
m1 --> m12
m2 --> m6
m2 --> m8
m2 --> m13
m3 --> m2
m3 --> m4
m3 --> m6
m3 --> m8
m3 --> m10
m3 --> m11
m3 --> m12
m3 --> m13
m4 --> m8
m4 --> m11
m4 --> m12
m5 --> m6
m5 --> m9
m5 --> m11
m5 --> m13
m6 --> m8
m7 --> m3
m7 --> m6
m7 --> m8
m8 --> m5
m8 --> m6
m8 --> m9
m8 --> m11
m8 --> m12
m8 --> m13
m9 --> m11
m9 --> m13
m10 --> m0
m10 --> m4
m10 --> m6
m10 --> m8
m10 --> m11
m10 --> m12
m11 --> m2
m11 --> m4
m11 --> m8
m11 --> m12
m12 --> m11
m12 --> m13
m13 --> m11
m13 --> m12Components by module
Most connected components
Top 20 of 23 by exported API surface and dependency count.
| Component | Module | Key exports |
|---|---|---|
_client.py | _client | _is_https_redirect, _port_or_default, _same_origin, UseClientDefault |
_exceptions.py | _exceptions | HTTPError, RequestError, TransportError, TimeoutException |
_main.py | _main | print_help, get_lexer_for_response, format_request_headers, format_response_headers |
_models.py | _models | _is_known_encoding, _normalize_header_key, _normalize_header_value, _parse_content_type_charset |
_transports/default.py | _transports | _load_httpcore_exceptions, map_httpcore_exceptions, ResponseStream, HTTPTransport |
_content.py | _content | ByteStream, IteratorByteStream, AsyncIteratorByteStream, UnattachedStream |
_decoders.py | _decoders | ContentDecoder, IdentityDecoder, DeflateDecoder, GZipDecoder |
_auth.py | _auth | Auth, FunctionAuth, BasicAuth, NetRCAuth |
_utils.py | _utils | primitive_value_to_str, get_environment_proxies, to_bytes, to_str |
_api.py | _api | request, stream, get, options |
_urlparse.py | _urlparse | ParseResult, urlparse, encode_host, normalize_port |
_multipart.py | _multipart | _format_form_param, _guess_content_type, get_multipart_boundary_from_content_type, DataField |
_config.py | _config | UnsetType, create_ssl_context, Timeout, Limits |
_transports/asgi.py | _transports | is_running_trio, create_event, ASGIResponseStream, ASGITransport |
__init__.py | __init__ | — |
_transports/wsgi.py | _transports | _skip_leading_empty_chunks, WSGIByteStream, WSGITransport |
_urls.py | _urls | URL, QueryParams |
_types.py | _types | SyncByteStream, AsyncByteStream |
_transports/base.py | _transports | BaseTransport, AsyncBaseTransport |
_transports/mock.py | _transports | MockTransport |
Declared runtime dependencies
What the structure teaches
One public door, 22 private rooms
Every module except __init__.py is underscore-private. That's not decoration — it gives the maintainers license to refactor anything internal without a deprecation cycle, because the only supported import path is the top-level package.
The transport layer is the extension point
_transports/ separates "what request to make" from "how bytes move": the default transport wraps httpcore, but the same interface admits mock transports for testing, WSGI/ASGI transports for calling your own app in-process, and custom ones. It's the most architecturally deliberate subpackage in the library.
Sync and async share one brain
_client.py holds both Client and AsyncClient, sharing request building, redirect logic, and cookie handling, diverging only where I/O actually happens. The x-ray shows it as the largest, most-connected component — the cost of not maintaining two parallel client codebases.
X-ray your own repo
This page is unedited archsteer xray output plus commentary. The same map of your codebase takes one command, runs locally, and never sends code anywhere: pip install archsteer && archsteer xray
FAQ
How is HTTPX structured internally?
As 23 underscore-private modules re-exported through a single __init__.py: _client.py (sync and async clients sharing one implementation), _models.py (Request/Response), _transports/ (the pluggable layer that actually moves bytes, wrapping httpcore by default), plus focused modules for auth, config, content, and URLs.
What is the HTTPX transport API?
A small interface (handle_request / handle_async_request) that separates the client's HTTP semantics from byte transport. Swapping it lets you test against a MockTransport, call an ASGI/WSGI app in-process without a socket, or add custom behavior — all without touching client code.
How was this architecture map generated?
By running `archsteer xray` (an open-source, MIT-licensed CLI) against the httpx/ package directory of the public GitHub repo — read-only static analysis, no code executed. Reproduce it with `pip install archsteer && archsteer xray`.
More x-rays
The architecture of FastAPI, x-rayed
An auto-generated architecture map of FastAPI: 48 components across 28 modules, the module…
The architecture of Flask, x-rayed
An auto-generated architecture map of Flask: 24 components, the sansio core split, bluepri…
The architecture of Starlette, x-rayed
An auto-generated architecture map of Starlette: 34 components, the middleware stack, and …
The architecture of Pydantic, x-rayed
An auto-generated architecture map of Pydantic: 104 components, the _internal core, the bu…