If you’ve worked with APIs in Python, you’re likely familiar with the requests
library. But what if you could get the same ease of use, asynchronous support, HTTP/2 performance gains, and advanced configuration options? That’s HTTPX exactly what offers.
HTTPXis a powerful HTTP client that supports synchronous and asynchronous requests, making it an excellent choice for handling API interactions at any scale.
It provides built-in authentication, connection pooling, and streaming responses, making working with modern web services easier.
This guide will walk you through integrating HTTPX into your Python projects and maximizing its features to handle HTTP requests efficiently.
Prerequisites
Before diving in, install Python 3.13 or higher. While this guide assumes some familiarity with making HTTP requests in Python—especially with the requests library—you can still follow along even if you're new to it.
Getting started with HTTPX
For the best learning experience, create a new Python project to experiment with the concepts covered in this tutorial.
Start by setting up a new directory and initializing a virtual environment:
mkdir httpx-demo && cd httpx-demo
python3 -m venv venv
Activate the virtual environment:
source venv/bin/activate
Following that, install the latest version of httpx
with the command below:
pip install httpx
Create a new client.py
file in the root of your project directory and add the following code:
import httpx
def get_data():
response = httpx.get('https://httpbin.org/get')
return response.json()
if __name__ == "__main__":
data = get_data()
print(data)
This snippet imports the httpx
package and defines a simple function that makes a GET request to a test endpoint.
Let's go ahead and run this script:
python client.py
You should observe output similar to the following:
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-httpx/0.28.1",
"X-Amzn-Trace-Id": "Root=1-67d400f3-23aca99e43e19bf3142ec86e"
},
"origin": "105.234.160.4",
"url": "https://httpbin.org/get"
}
The response from httpbin.org
contains information about our request, including the sent headers. This simple example demonstrates the ease of making HTTP requests with HTTPX.
Understanding HTTPX response objects
When you make an HTTP request using httpx
, the response object contains important information about the server’s reply. This includes the status code, headers, and response body, which help determine whether the request was successful and how to process the returned data.
Modify your client.py
file to inspect different parts of the response:
import httpx
def get_data():
response = httpx.get("https://httpbin.org/get")
print(f"Status Code: {response.status_code}")
print(f"Headers: {response.headers}")
print(f"Content: {response.text}")
return response
if __name__ == "__main__":
get_data()
This script retrieves data from httpbin.org
and displays important response details. Here’s what each part of the response object represents:
response.status_code
– Indicates the HTTP status of the request (e.g.,200
for success,404
for not found).response.headers
– A dictionary containing metadata about the response, such as content type, server information, and caching policies.response.text
– The full response body returned as a string.response.json()
– Parses the response body as JSON, converting it into a Python dictionary for easy data manipulation.
After running the script, you should see something like:
Status Code: 200
Headers: Headers({'date': 'Fri, 14 Mar 2025 10:16:31 GMT', 'content-type': 'application/json', 'content-length': '304', 'connection': 'keep-alive', 'server': 'gunicorn/19.9.0', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true'})
Content: {
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-httpx/0.28.1",
"X-Amzn-Trace-Id": "Root=1-67d401ff-6588f575574b55e6172d74ae"
},
"origin": "105.234.160.4",
"url": "https://httpbin.org/get"
}
<Response [200 OK]>
The response output consists of three key parts:
- Status Code – Indicates whether the request was successful. A
200
status means success, while a404
suggests that the requested resource was not found, and a500
points to a server error. - Headers – Provide metadata about the response, including details like
Content-Type
, which specifies the format of the response, andServer
, which identifies the web server handling the request. - Content – Contains the actual response body, typically a JSON object that includes valuable details about the request, such as the headers sent and the request's origin.
Now that you understand response objects, let's explore how to make different types of HTTP requests using HTTPX.
Understanding basic request methods
Now that you understand how HTTPX handles responses, let's explore different types of HTTP requests beyond GET
.
HTTPX supports a variety of methods, including POST
, PUT
, DELETE
, and query parameters with GET
, allowing you to interact with APIs that require data submission or modifications.
A POST
request is typically used to send data to a server, such as creating a new resource or submitting a form.
Remove all contents from client.py
and replace it with the following code to send a POST
request with JSON data:
import httpx
def create_task():
url = "https://httpbin.org/post"
payload = {"title": "Learn HTTPX", "completed": False}
response = httpx.post(url, json=payload)
print(f"Status Code: {response.status_code}")
print(f"Response: {response.json()}")
return response
if __name__ == "__main__":
create_task()
Run the script:
python client.py
You should see output similar to this:
Status Code: 200
Response: {
"args": {},
"data": "{\"title\":\"Learn HTTPX\",\"completed\":false}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "41",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "python-httpx/0.28.1",
"X-Amzn-Trace-Id": "Root=1-67d403c3-7dff0dd26aa30e9c3f88df47"
},
"json": {
"completed": false,
"title": "Learn HTTPX"
},
"origin": "105.234.160.4",
"url": "https://httpbin.org/post"
}
The response confirms that your request was processed successfully. The json
field in the response body contains the data you sent, proving that the server received it correctly.
The headers also indicate that the request was sent as application/json
, thanks to HTTPX automatically setting the correct content type.
While POST
is used to create new resources, a PUT
request is used to update existing ones. When you need to modify an entry rather than add a new one, PUT
ensures that the existing data is replaced with the updated values.
Here’s an example of making a PUT
request with HTTPX:
url = "https://httpbin.org/put"
payload = {"title": "Learn HTTPX", "completed": True}
response = httpx.put(url, json=payload)
This request updates an existing resource with new data, ensuring changes are correctly reflected on the server.
If you need to remove a resource instead, a DELETE
request is used. This is particularly useful for deleting records from a database or removing items from an API:
url = "https://httpbin.org/delete"
response = httpx.delete(url)
Handling query parameters in a GET request
Sometimes, API requests need additional information in the URL, such as filtering or searching for specific data. This is done using query parameters.
Modify client.py
to send a GET
request with query parameters:
import httpx
def get_filtered_tasks():
url = "https://httpbin.org/get"
params = {"status": "completed"}
response = httpx.get(url, params=params)
print(f"Status Code: {response.status_code}")
print(f"Response: {response.json()}")
return response
if __name__ == "__main__":
get_filtered_tasks()
Run the script:
python client.py
You should see output like:
Status Code: 200
Response: {'args': {'status': 'completed'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-httpx/0.28.1', 'X-Amzn-Trace-Id': 'Root=1-67d406c3-6b4d928e394b3353355228af'}, 'origin': '105.234.160.4', 'url': 'https://httpbin.org/get?status=completed'}
The "args"
field in the response confirms that the query parameter status=completed
was sent successfully. This is useful when filtering or searching through API results.
Now that you've learned how to send different HTTP requests, the next step is to ensure your requests are resilient.
Handling timeouts and retries in HTTPX
Network issues, slow server responses, or connection failures can impact your application's performance. HTTPX provides built-in support for timeouts, retries, and error handling, which helps ensure your requests remain stable and reliable.
Setting a timeout for requests
By default, HTTPX waits indefinitely for a response, which can cause your program to hang if the server is slow or unresponsive.
You can prevent this by setting a timeout, which defines how long HTTPX should wait before giving up on a request.
Replace all the contents in the client.py
file with the following to implement a timeout:
import httpx
def fetch_with_timeout():
url = "https://httpbin.org/delay/5" # Simulates a delayed response
try:
response = httpx.get(url, timeout=3) # Timeout set to 3 seconds
print(f"Status Code: {response.status_code}")
print(f"Response: {response.json()}")
except httpx.TimeoutException:
print("Request timed out!")
if __name__ == "__main__":
fetch_with_timeout()
If the server responds within 3 seconds, the request succeeds, and the response is printed. If the server takes longer than 3 seconds, HTTPX raises a TimeoutException
, and the program prints "Request timed out!"
.
Run the script:
python client.py
If the request exceeds the timeout, you'll see:
Request timed out!
This is useful for preventing unresponsive requests from blocking your application.
Retrying failed requests
Sometimes, network failures or temporary server issues cause requests to fail. HTTPX allows you to retry failed requests automatically using an httpx.Client
with a custom retry strategy.
Remove all existing code in the client.py
file and update it with the following to include retries:
import httpx
from httpx import RequestError
def fetch_with_retries():
url = "https://httpbin.org/status/500" # Simulates a server error
with httpx.Client() as client:
for attempt in range(3): # Retry up to 3 times
try:
response = client.get(url)
response.raise_for_status() # Raise an error for bad responses (e.g., 500)
print(f"Status Code: {response.status_code}")
return response.json()
except httpx.HTTPStatusError as e:
print(f"Attempt {attempt + 1}: Received {e.response.status_code} - Retrying...")
except RequestError:
print("Network error occurred - Retrying...")
print("All retry attempts failed.")
if __name__ == "__main__":
fetch_with_retries()
The script attempts to send a request to an endpoint that returns a 500
error. If an error occurs, it retries up to 3 times before giving up.
The raise_for_status()
method ensures we only process successful responses.
If all retry attempts fail, it prints "All retry attempts failed."
Run the script:
python client.py
Expected output:
Attempt 1: Received 500 - Retrying...
Attempt 2: Received 500 - Retrying...
Attempt 3: Received 500 - Retrying...
All retry attempts failed.
Implementing retries improves your application's resilience to temporary network issues.
Working with authentication in HTTPX
Now that you have learned how to handle timeouts and retries in HTTPX, the next step is securing API requests. Many APIs require authentication before allowing access to protected resources. HTTPX supports various authentication methods, such as Basic Auth, Token-based authentication, and OAuth.
Using basic authentication
Basic Authentication (Basic Auth
) is one of the simplest forms of authentication, where the client sends a username and password encoded in the request headers.
Replace the contents of client.py
with the following code to use Basic Authentication:
import httpx
def fetch_with_auth():
url = "https://httpbin.org/basic-auth/user/pass"
auth = ("user", "pass") # Replace with your credentials
response = httpx.get(url, auth=auth)
print(f"Status Code: {response.status_code}")
print(f"Response: {response.json()}")
if __name__ == "__main__":
fetch_with_auth()
The auth
argument accepts a tuple containing the username and password. HTTPX automatically encodes the credentials into the Authorization
header as Basic base64(username:password)
.
Then the server verifies the credentials and responds accordingly.
Run the script with the following:
python client.py
Expected output:
Status Code: 200
Response: {'authenticated': True, 'user': 'user'}
This confirms that the authentication was successful. If the credentials are incorrect, the server will return a 401 Unauthorized
response.
Using Bearer Token authentication
Many APIs use token-based authentication (e.g., API keys, JWT tokens) instead of Basic Auth. Tokens are typically passed in the Authorization
header.
Modify client.py
to send an API request with a Bearer Token:
import httpx
def fetch_with_auth():
url = "https://httpbin.org/bearer"
headers = {
"Authorization": "Bearer your_api_token_here" # Replace with your token
}
response = httpx.get(url, headers=headers)
print(f"Status Code: {response.status_code}")
print(f"Response: {response.json()}")
if __name__ == "__main__":
fetch_with_auth()
The Authorization header is manually set to "Bearer <token>"
.
The server verifies the token and responds accordingly.
Run the script like you have been doing:
python client.py
Expected output:
Status Code: 200
Response: {'authenticated': True, 'token': 'your_api_token_here'}
If the token is missing or invalid, you'll receive a 403 Forbidden
or 401 Unauthorized
response.
Asynchronous requests with HTTPX
So far, you've been making synchronous HTTP requests, meaning each request blocks the program until it responds. This is fine for small-scale applications, but asynchronous programming can significantly improve performance when dealing with multiple API calls or high-latency networks.
HTTPX provides native async support using Python’s asyncio
and async/await
syntax, allowing requests to be made concurrently instead of sequentially.
Asynchronous requests are beneficial when:
- You need to make multiple API calls concurrently (e.g., fetching data from multiple endpoints).
- Your application handles high-latency network requests.
- You want to improve efficiency without blocking execution.
Instead of waiting for each request to finish before making the next one, async HTTPX allows multiple requests to be sent simultaneously, significantly reducing wait times.
Making an asynchronous GET request
Replace the contents of client.py
with the following to send an asynchronous GET request:
import httpx
import asyncio
async def fetch_data():
url = "https://httpbin.org/get"
async with httpx.AsyncClient() as client:
response = await client.get(url)
print(f"Status Code: {response.status_code}")
print(f"Response: {response.json()}")
if __name__ == "__main__":
asyncio.run(fetch_data())
The fetch_data()
function is an asynchronous coroutine that makes a non-blocking GET request.
The async with httpx.AsyncClient() as client
statement ensures proper session management, while await client.get(url)
sends the request without pausing execution.
Finally, asyncio.run(fetch_data())
starts the event loop, allowing multiple API calls to run concurrently for improved performance.
Run the script:
python client.py
Expected output:
Status Code: 200
Response: {'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-httpx/0.28.1', 'X-Amzn-Trace-Id': 'Root=1-67d40ee1-24d1e7e30a55bf8f49edd768'}, 'origin': '105.234.160.4', 'url': 'https://httpbin.org/get'}
The output confirms a successful async request with Status Code: 200
, displaying request metadata, source IP, and the requested URL—showcasing non-blocking execution.
Making multiple asynchronous requests
One of the most significant advantages of async requests is that they allow multiple API calls to be made concurrently. Instead of sending them individually, all requests are sent simultaneously, reducing total execution time.
Modify client.py
to send multiple GET requests concurrently:
import httpx
import asyncio
import time
async def fetch_url(url, client):
response = await client.get(url)
return url, response.status_code
async def main():
urls = [
"https://httpbin.org/get",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
]
start_time = time.time()
# Using an async client as a context manager
async with httpx.AsyncClient() as client:
# Create a list of tasks
tasks = [fetch_url(url, client) for url in urls]
# Gather all tasks (execute them concurrently)
results = await asyncio.gather(*tasks)
# Print results
for url, status_code in results:
print(f"URL: {url}, Status: {status_code}")
end_time = time.time()
print(f"Total time: {end_time - start_time:.2f} seconds")
if __name__ == "__main__":
asyncio.run(main())
This program defines fetch_url()
, an asynchronous function that requests a GET to a given URL and returns its response status. The main()
function builds a list of URLs and sends all requests concurrently using asyncio.gather(*tasks)
, ensuring they run in parallel.
An AsyncClient
is used within a context manager to manage connections efficiently. By executing all requests simultaneously, the program avoids delays in sequential execution.
Rerun the program and you will see output like this:
URL: https://httpbin.org/get, Status: 200
URL: https://httpbin.org/delay/1, Status: 200
URL: https://httpbin.org/delay/2, Status: 200
URL: https://httpbin.org/delay/3, Status: 200
Total time: 5.99 seconds
In a synchronous approach, each request would complete before the next one starts, leading to a total execution time roughly equal to the sum of all delays.
However, with async execution, all requests run simultaneously, drastically reducing the overall wait time.
This method is handy when working with APIs involving network latency or when handling high requests efficiently.
Leveraging HTTP/2 with HTTPX
HTTP/2 is a major iteration of the HTTP protocol that provides a far more efficient transport layer with significant performance benefits. Unlike HTTP/1.1's text-based format, HTTP/2 uses a binary format that enables:
- Multiplexing: Multiple requests and responses can share a single TCP connection simultaneously
- Header compression: Reduces overhead by efficiently compressing HTTP headers
- Stream prioritization: Allows clients to indicate which resources are more important
- Server push: Enables servers to proactively send resources to clients before they're explicitly requested
These improvements can dramatically enhance performance, especially for applications making multiple concurrent requests.
HTTPX doesn't enable HTTP/2 by default, as HTTP/1.1 is considered the more mature and battle-tested option. To use HTTP/2, you first need to install the required dependencies:
pip install 'httpx[http2]'
Once installed, you can enable HTTP/2 by setting the http2
parameter to True
when creating a client:
import httpx
def use_http2():
# Create a client with HTTP/2 support enabled
with httpx.Client(http2=True) as client:
# Make a request
response = client.get("https://nghttp2.org/httpbin/get")
# Check which HTTP version was used
print(f"HTTP Version: {response.http_version}")
print(f"Status Code: {response.status_code}")
print(f"Response: {response.json()}")
if __name__ == "__main__":
use_http2()
When you run this script, you should see output like this:
HTTP Version: HTTP/2
Status Code: 200
Response: {'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'nghttp2.org', 'User-Agent': 'python-httpx/0.28.1'}, 'origin': '105.234.160.4', 'url': 'https://nghttp2.org/httpbin/get'}
It's important to note that enabling HTTP/2 in your HTTPX client does not guarantee that your requests will use HTTP/2. Both the client and server must support HTTP/2 for it to be used.
If you connect to a server that only supports HTTP/1.1, the client will automatically fall back to using HTTP/1.1.
You can always check which version of the HTTP protocol was actually used by examining the .http_version
property on the response:
import httpx
def check_http_version(url):
with httpx.Client(http2=True) as client:
response = client.get(url)
print(f"URL: {url}")
print(f"HTTP Version: {response.http_version}")
print(f"Status Code: {response.status_code}")
print()
if __name__ == "__main__":
# Check various websites for HTTP/2 support
urls = [
"https://nghttp2.org/httpbin/get", # Supports HTTP/2
"https://httpbin.org/get", # May or may not support HTTP/2
"http://example.org/" # Likely HTTP/1.1 (note: using HTTP, not HTTPS)
]
for url in urls:
check_http_version(url)
Running this script will show which HTTP version was used for each request:
URL: https://nghttp2.org/httpbin/get
HTTP Version: HTTP/2
Status Code: 200
URL: https://httpbin.org/get
HTTP Version: HTTP/2
Status Code: 200
URL: http://example.org/
HTTP Version: HTTP/1.1
Status Code: 200
This demonstrates how HTTPX automatically selects the appropriate protocol based on server support.
Final thoughts
This guide covered HTTPX's key features, from basic requests to advanced capabilities like authentication, async operations, and HTTP/2 support. HTTPX bridges the gap between the familiar requests library API and modern Python development needs.
Its intuitive interface and powerful features make HTTPX suitable for everything from simple scripts to complex service architectures.
For more details, refer to the official HTTPX documentation. Happy coding!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github