By the Octri team · Updated
httpx vs requests vs aiohttp: which Python HTTP client to use
Choosing an HTTP library for a Python API integration is less about which one can send a GET request and more about the shape of the application around it. A one-off script, an async service and a distributed SDK have different expectations for connection reuse, timeouts, cancellation and testing.
httpx vs requests vs aiohttp: which should you use?
requests remains a straightforward choice for synchronous scripts and services that already rely on it. httpx offers closely related synchronous and asynchronous client APIs, built-in test transports and optional HTTP/2 support. aiohttp is an established async-first client with its own session model and a web-server ecosystem.
For a Python SDK that promises both blocking and async usage, HTTPX is often convenient because one family of request and response concepts can support both clients. That convenience does not eliminate the need to test each path.

httpx vs requests vs aiohttp at a glance
| Consideration | requests | httpx | aiohttp |
|---|---|---|---|
| Blocking requests | Yes | Yes | Not its main client API |
| Native async | No | Yes | Yes |
| Timeout if omitted | No default timeout | Network inactivity timeout, normally 5s | Default total timeout, normally 300s |
| HTTP/2 client | Not built in | Optional | Not built in |
| Mock transport | Adapter/mocking tools | MockTransport |
Separate test tools |
Defaults can change with versions. Check the version of each dependency you're packaging rather than assuming an old benchmark or example still applies.
The same request in each library
I sent the same request against a local HTTP server with all three libraries. A requests.Session, an httpx.Client and an aiohttp.ClientSession each returned 200 from /v1/orders. That proved the basic call path, not that one client was faster; loopback timing wouldn't be a meaningful comparison of internet performance.
# Synchronous requests
with requests.Session() as session:
r = session.get(base_url + '/v1/orders', timeout=2)
r.raise_for_status()
# Synchronous httpx
with httpx.Client(base_url=base_url, timeout=2) as client:
r = client.get('/v1/orders')
r.raise_for_status()And the asynchronous form requires an event loop and an open session:
async with aiohttp.ClientSession() as session:
async with session.get(base_url + '/v1/orders',
timeout=aiohttp.ClientTimeout(total=2)) as r:
r.raise_for_status()
data = await r.json()The three clients reach the same HTTP API. Their differences become more important as an application grows.
Async and concurrent requests
Async code can overlap network waits without using one worker thread per request. Both HTTPX AsyncClient and aiohttp support connection pooling and concurrent requests, provided you reuse a client or session instead of constructing one per operation. Calling a synchronous requests.get() directly inside an async handler can block the event loop; use an async client or intentionally offload blocking work.
If your SDK exposes Acme and AsyncAcme, document which dependencies back each and make their behavior agree on error types, retry policy and parsed models.
Timeouts
The meaningful distinction is between connection, read, write, pool and total timeouts. A five-second period of network inactivity is not necessarily a five-second cap on a complete download. HTTPX provides distinct timeout settings; aiohttp supports total and phase-specific controls; requests accepts connect/read timeouts but does not enforce a whole-response wall-clock deadline by default.
An SDK should select deliberate defaults and let callers override them. A never-ending call is rarely a good default for a service integration.
Retries
Do not assume a library will retry every server error for you. Requests can use urllib3's Retry through an adapter; HTTPX provides limited transport-level retries and often needs an application policy for status responses; aiohttp commonly relies on explicit retry logic or an extension. Classify idempotent reads separately from writes, honor Retry-After, and preserve one idempotency key across attempts when the API supports it.
Testing code that makes HTTP calls
HTTPX's MockTransport is handy for an SDK because you can record the exact Request and return canned responses without a socket. I used it in a local test to check an Authorization header and full URL; the assertion passed. Requests has adapters and tools such as responses, while aiohttp projects often use aioresponses or a local test server.
A library that permits injecting its transport makes failures easier to reproduce than one that constructs a hidden global client on every call.
Moving from requests to httpx
The method names look familiar, but differences deserve a deliberate review: redirect behavior, exception classes, timeout semantics, proxies, streaming and client lifetime. Start with the small part of your program that makes network calls, preserve the behavior with mock tests, then swap the transport. A search-and-replace of imports may run but subtly change production behavior.
If you are building a Python SDK
Choose the HTTP engine for the public contract you're willing to support. If both synchronous and asynchronous interfaces matter, HTTPX reduces duplication. If your audience is almost entirely synchronous and already has extensive requests infrastructure, requests is perfectly defensible. An async-only SDK can work well with aiohttp, but be honest in its documentation about that choice.

Checklist for calling APIs from Python
Reuse sessions, set timeouts, preserve credentials safely, surface request IDs and distinguish connection failures from status errors. Test a failed request, a stream and a multi-page response. Make retries explicit and avoid leaking a session beyond its intended lifetime.
Python SDKs with Octri
Octri's SDK settings can help align generated Python clients with the HTTP transport and initialization style that fit your audience. Choose those settings because of user needs and tested behavior, then expose the decisions in the package README.
