Python httpx
last modified October 30, 2026 Python 3.15
In this article we show how to create HTTP requests in Python with the
httpx module. The httpx module allows to create both
synchronous and asynchronous HTTP requests.
The httpx module
HTTPX is an HTTP client for Python, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2. It has an API similar to the popular requests library. Unlike requests, httpx has timeouts enabled by default and does not follow redirects unless asked to.
With the combination of httpx and the asyncio module, we can generate asynchronous web requests. This may lead to a considerable increase of efficiency in programs that send many requests.
In the examples, we use httpbin.org, a freely available HTTP
request and response service, and example.com, a small HTML
page reserved for documentation examples. The HTTP/2 support requires the
optional http2 extra.
$ pip install 'httpx[http2]'
Python httpx status code
In the first example, we determine the status of a web page. The status
code is determined with the status_code property.
import httpx
r = httpx.head('https://example.com')
print(r.status_code)
print(r.reason_phrase)
print(r.http_version)
print(r.is_success)
The example creates a synchronous HEAD request to the example.com website
and retrieves an HTTP response. A HEAD request returns only the headers,
without the body. From the response, we get the status code, the reason
phrase, and the HTTP version. The is_success property is
True for 2xx status codes.
$ python sync_status.py 200 OK HTTP/1.1 True
Python httpx GET request
The following example creates a synchronous GET request.
import httpx
r = httpx.get('https://example.com')
print(r.status_code)
print(r.headers['content-type'])
print(r.encoding)
print(len(r.content), 'bytes')
start = r.text.index('<title>') + len('<title>')
end = r.text.index('</title>')
print(r.text[start:end])
We generate a GET request to a web page with the httpx.get
function. The content property contains the raw body as
bytes; the text property contains the body decoded with the
encoding from the Content-Type header. We print the length of
the document and its title.
$ python sync_get.py 200 text/html; charset=utf-8 utf-8 577 bytes Example Domain
Python httpx query parameters
The params option sends query parameters with the request.
import httpx
payload = {'name': 'John Doe', 'occupation': 'gardener'}
r = httpx.get('https://httpbin.org/get', params=payload)
print(r.url)
data = r.json()
print(data['args'])
print(data['headers']['User-Agent'])
The parameters are encoded into the query string of the URL. The
json method decodes the JSON body of the response. httpbin.org
returns the received parameters in the args field and the
request headers in the headers field.
$ python sync_query_params.py
https://httpbin.org/get?name=John+Doe&occupation=gardener
{'name': 'John Doe', 'occupation': 'gardener'}
python-httpx/0.28.1
Python httpx POST form request
A POST request is generated with the httpx.post function.
With the application/x-www-form-urlencoded content type, the
data is sent in the body of the request; the keys and values are encoded
in key-value pairs separated by &, with a =
between the key and the value.
import httpx
payload = {'name': 'John Doe', 'occupation': 'gardener'}
r = httpx.post('https://httpbin.org/post', data=payload)
data = r.json()
print(data['form'])
print(data['headers']['Content-Type'])
We generate a synchronous POST request with form data to
httpbin.org/post. The payload is set to the data
option.
$ python sync_post_form.py
{'name': 'John Doe', 'occupation': 'gardener'}
application/x-www-form-urlencoded
Python httpx POST JSON request
The json option sends JSON data.
import httpx
payload = {'int': 123, 'boolean': True, 'list': ['a', 'b', 'c']}
r = httpx.post('https://httpbin.org/post', json=payload)
data = r.json()
print(data['json'])
print(data['headers']['Content-Type'])
The Python dictionary is serialized to JSON and the
application/json content type is set. httpbin.org returns the
parsed data in the json field, with the keys sorted
alphabetically.
$ python sync_post_json.py
{'boolean': True, 'int': 123, 'list': ['a', 'b', 'c']}
application/json
Python httpx request headers
Custom request headers are passed in the headers option.
import httpx
headers = {'User-Agent': 'zetcode-client/1.0', 'Accept': 'application/json'}
r = httpx.get('https://httpbin.org/headers', headers=headers)
for name, value in r.json()['headers'].items():
if not name.startswith('X-Amzn'):
print(f'{name}: {value}')
We set a custom user agent and the accepted content type. The
httpbin.org/headers endpoint returns the headers it received;
we skip the tracing headers added by the load balancer.
$ python headers.py Accept: application/json Accept-Encoding: gzip, deflate Host: httpbin.org User-Agent: zetcode-client/1.0
Python httpx Client
The top-level functions such as httpx.get open a new
connection for each request. The httpx.Client reuses
connections and shares configuration among requests.
import httpx
with httpx.Client(base_url='https://httpbin.org',
headers={'User-Agent': 'zetcode-client/1.0'},
params={'lang': 'en'},
timeout=10.0) as client:
r1 = client.get('/get', params={'page': 1})
r2 = client.get('/user-agent')
print(r1.url)
print(r1.json()['args'])
print(r2.json())
The client has a base URL, a default header, a default query parameter, and a timeout. Relative URLs are joined with the base URL, and the default query parameters are merged with the parameters of each request. Using the client as a context manager closes its connections at the end.
$ python client.py
https://httpbin.org/get?lang=en&page=1
{'lang': 'en', 'page': '1'}
{'user-agent': 'zetcode-client/1.0'}
Python httpx redirects
By default, httpx does not follow redirects. The
follow_redirects option enables it.
import httpx
r = httpx.get('https://httpbin.org/redirect/2')
print(r.status_code, r.headers['location'])
r = httpx.get('https://httpbin.org/redirect/2', follow_redirects=True)
print(r.status_code, r.url)
for resp in r.history:
print(resp.status_code, resp.url)
Without following, we get the 302 response with the Location
header. With follow_redirects=True, httpx follows the
redirects to the final page; the intermediate responses are stored in the
history list.
$ python redirects.py 302 /relative-redirect/1 200 https://httpbin.org/get 302 https://httpbin.org/redirect/2 302 https://httpbin.org/relative-redirect/1
Python httpx errors
httpx does not raise an exception for error status codes; we call
raise_for_status. Network problems raise exceptions derived
from httpx.TransportError.
import httpx
try:
r = httpx.get('https://httpbin.org/status/404')
print(r.status_code, r.is_error)
r.raise_for_status()
except httpx.HTTPStatusError as e:
print(e)
try:
httpx.get('https://httpbin.org/delay/5', timeout=1.0)
except httpx.TimeoutException as e:
print(type(e).__name__, e)
try:
httpx.get('https://nonexistent.example/')
except httpx.ConnectError as e:
print(type(e).__name__, e)
The raise_for_status method raises
HTTPStatusError for 4xx and 5xx responses. The default
timeout is five seconds; here we set it to one second, so a request to an
endpoint that waits five seconds raises ReadTimeout. A host
that does not exist raises ConnectError.
$ python errors.py 404 True Client error '404 Not Found' for url 'https://httpbin.org/status/404' For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 ReadTimeout The read operation timed out ConnectError [Errno -2] Name or service not known
Python httpx stream data
For larger downloads, we can stream responses that do not load the entire
response body into memory at once. For streaming, we use the
httpx.stream function.
import httpx
url = 'https://httpbin.org/bytes/102400'
with open('data.bin', 'wb') as f:
with httpx.stream('GET', url) as r:
total = int(r.headers['content-length'])
size = 0
for chunk in r.iter_bytes(chunk_size=32768):
f.write(chunk)
size += len(chunk)
print(f'{size:>6} / {total} bytes')
The httpbin.org/bytes/n endpoint returns n random
bytes. We iterate over the binary content in chunks of 32 KiB with the
iter_bytes method, write them to a file, and print the
progress. The total size is taken from the Content-Length
header.
$ python sync_stream.py 32768 / 102400 bytes 65536 / 102400 bytes 98304 / 102400 bytes 102400 / 102400 bytes
Python httpx HTTP/2
HTTP/2 is enabled with the http2 option of the client. It
requires the h2 package, installed with the
http2 extra.
import httpx
with httpx.Client(http2=True) as client:
r = client.get('https://example.com')
print(r.http_version)
with httpx.Client() as client:
r = client.get('https://example.com')
print(r.http_version)
The protocol is negotiated during the TLS handshake. If the server supports HTTP/2, the request uses it; otherwise, httpx falls back to HTTP/1.1. Without the option, httpx uses HTTP/1.1.
$ python http2.py HTTP/2 HTTP/1.1
Testing with MockTransport
The transport is the part of the client that sends requests. The
MockTransport class calls a function instead of using the
network, which is useful in tests.
import httpx
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == '/users/1':
return httpx.Response(200, json={'id': 1, 'name': 'John Doe'})
return httpx.Response(404, json={'error': 'not found'})
def get_user_name(client: httpx.Client, user_id: int) -> str | None:
r = client.get(f'/users/{user_id}')
if r.status_code == 404:
return None
return r.json()['name']
transport = httpx.MockTransport(handler)
with httpx.Client(transport=transport, base_url='https://api.example.com') as client:
print(get_user_name(client, 1))
print(get_user_name(client, 2))
The handler function receives the request and returns a
response. The get_user_name function works with any client, so
in the test we pass it a client with the mock transport. No network
connection is made.
$ python mock_transport.py John Doe None
Python httpx async GET request
The following example generates a simple asynchronous GET request.
import asyncio
import httpx
async def main():
async with httpx.AsyncClient() as client:
r = await client.get('https://httpbin.org/get', params={'q': 'async'})
print(r.status_code)
print(r.json()['args'])
asyncio.run(main())
With async def, we create a coroutine. We create an
asynchronous HTTP client with httpx.AsyncClient; it has the
same methods as the synchronous client, but they are coroutines. With the
await keyword, the get coroutine is started and
the control is given back to the event loop until the response arrives; in
the meantime, the event loop can run other tasks. The
asyncio.run function starts the event loop and runs the
main coroutine.
$ python async_get.py
200
{'q': 'async'}
Python httpx multiple asynchronous GET requests
The asyncio.gather function runs coroutines concurrently.
import asyncio
import httpx
urls = ['https://example.com', 'https://httpbin.org/get',
'https://httpbin.org/html', 'https://httpbin.org/status/404']
async def main():
async with httpx.AsyncClient() as client:
resps = await asyncio.gather(*(client.get(url) for url in urls))
for resp in resps:
print(resp.status_code, resp.headers['content-type'], resp.url)
asyncio.run(main())
We create one asynchronous client and send four GET requests through it.
The generator expression creates a coroutine for each URL; they are
unpacked into the arguments of gather with the *
operator. When all coroutines are completed, gather returns
the responses in the order of the URLs.
$ python async_mul_get.py 200 text/html; charset=utf-8 https://example.com 200 application/json https://httpbin.org/get 200 text/html; charset=utf-8 https://httpbin.org/html 404 text/html; charset=utf-8 https://httpbin.org/status/404
Python httpx async POST JSON request
The following example shows how to send an asynchronous POST request with JSON data.
import asyncio
import httpx
async def main():
data = {'int': 123, 'boolean': True, 'list': ['a', 'b', 'c']}
async with httpx.AsyncClient() as client:
r = await client.post('https://httpbin.org/post', json=data)
print(r.json()['json'])
asyncio.run(main())
The data is set to the json option of the post
coroutine. Form data would be passed in the data option, as in
the synchronous API.
$ python async_post_json.py
{'boolean': True, 'int': 123, 'list': ['a', 'b', 'c']}
Python httpx asynchronous stream request
The example shows how to download a binary file in an asynchronous stream.
import asyncio
import httpx
url = 'https://httpbin.org/bytes/102400'
async def main():
size = 0
async with httpx.AsyncClient() as client:
async with client.stream('GET', url) as r:
with open('data.bin', 'wb') as f:
async for chunk in r.aiter_bytes():
f.write(chunk)
size += len(chunk)
print(f'downloaded {size} bytes')
asyncio.run(main())
We use the client.stream asynchronous context manager and
iterate over the chunks with async for and
aiter_bytes.
$ python async_stream.py downloaded 102400 bytes
Comparing synchronous and asynchronous requests
In the following two examples, we compare the efficiency of a group of
synchronous and asynchronous requests. The
httpbin.org/delay/n endpoint waits n seconds before
it responds.
import time
import httpx
urls = [f'https://httpbin.org/delay/{n}' for n in (1, 1, 2, 1, 1)]
start = time.perf_counter()
with httpx.Client(timeout=30) as client:
for url in urls:
r = client.get(url)
print(r.status_code, url)
print(f'Elapsed: {time.perf_counter() - start:.2f} s')
We generate five synchronous GET requests one after another and calculate
the elapsed time with time.perf_counter.
import asyncio
import time
import httpx
urls = [f'https://httpbin.org/delay/{n}' for n in (1, 1, 2, 1, 1)]
async def main():
async with httpx.AsyncClient(timeout=30) as client:
resps = await asyncio.gather(*(client.get(url) for url in urls))
for url, r in zip(urls, resps):
print(r.status_code, url)
start = time.perf_counter()
asyncio.run(main())
print(f'Elapsed: {time.perf_counter() - start:.2f} s')
We generate the same five GET requests asynchronously. The synchronous version takes at least the sum of the delays, six seconds, plus the network overhead. The asynchronous version waits for all responses at the same time, so it takes about as long as the slowest request.
$ python multiple_sync.py 200 https://httpbin.org/delay/1 200 https://httpbin.org/delay/1 200 https://httpbin.org/delay/2 200 https://httpbin.org/delay/1 200 https://httpbin.org/delay/1 Elapsed: 7.99 s $ python multiple_async.py 200 https://httpbin.org/delay/1 200 https://httpbin.org/delay/1 200 https://httpbin.org/delay/2 200 https://httpbin.org/delay/1 200 https://httpbin.org/delay/1 Elapsed: 2.78 s
Source
In this article we have generated synchronous and asynchronous web requests in Python with the httpx module.
Author
List all Python tutorials.