Consume REST APIs with httpx
Learn to consume REST APIs with httpx in this Python tutorial — hands-on steps, troubleshooting, and practical exercises for progressive mastery.
Focus: consume REST APIs with httpx
Imagine your Python script needs to pull the latest weather data, submit a user registration form, or fetch tweets from an API. You could use urllib.request or the ubiquitous requests library, but there's a modern contender that's built for the async age: httpx. If you're using requests for everything, you're missing out on cleaner async support, a more consistent interface, and the ability to throw in retries and timeouts without extra libraries. This lesson shows you how to consume REST APIs with httpx — synchronously and asynchronously — so you can make HTTP calls with confidence in 2024.
The problem this lesson solves
Most Python developers reach for requests when they need to call an API. It's a great library, but it has a few pain points that accumulate in real-world projects:
- No built-in async support. You have to switch to
aiohttporhttpxif you want non-blocking calls withasyncio. - Manual retry logic.
requestsdoesn't retry on failure; you have to wrap calls in a loop and handleConnectionErroryourself. - Inconsistent timeout handling. Timeouts apply to the whole connection, not to individual read or write operations.
- Verbose session management. You often write boilerplate to reuse connections.
httpx solves these by offering:
- A sync API that works exactly like
requestsfor quick scripts. - An async API using
async/awaitfor high-concurrency workloads. - Built-in support for timeouts, retries, and connection pooling.
- HTTP/1.1 and HTTP/2 out of the box.
The goal of this lesson is to make you productive with httpx so you can consume REST APIs with httpx in your Python projects without friction.
Core concept / mental model
Think of httpx as a Swiss Army knife for HTTP. It's a client library that lets you:
- Send GET, POST, PUT, PATCH, DELETE requests.
- Attach headers, query parameters, JSON bodies, and files.
- Handle responses with status codes, headers, and content.
- Manage connections via a client object (like a browser session).
The key mental model: httpx.Client is your session. It maintains connection pools, default headers, cookies, and authentication across requests. Without a client, each request opens a new connection (which is fine for one-off calls but wasteful for many).
A simple call looks like this:
import httpx
response = httpx.get('https://httpbin.org/json')
print(response.status_code) # 200
print(response.json()['slideshow']['title']) # "Sample Slide Show"
Under the hood, httpx performs a synchronous HTTP request, parses the response, and returns a Response object. The entire process follows the standard HTTP lifecycle:
- Open connection (or reuse from pool).
- Send request line + headers + body (if any).
- Receive response status + headers + body.
- Return
Response.
Pro tip: Use
with httpx.Client() as client:to automatically close connections and benefit from connection reuse.
How it works step by step
Let's break down a typical httpx GET request into its core components:
1. Import and instantiate
import httpx
httpx exposes top-level functions like .get(), .post(), but it's better to create a Client for production code.
2. Create a client (optionally)
with httpx.Client() as client:
response = client.get("https://api.example.com/items")
The Client object holds:
- base_url — to avoid repeating the host.
- headers — for common headers like Authorization.
- timeout — default timeout for all requests.
- cookies — persists across requests.
3. Send request with parameters
params = {"page": 1, "limit": 10}
response = client.get("/items", params=params)
params are automatically URL-encoded. You can also pass headers, cookies, and content for POST requests.
4. Handle the response
The Response object has many useful attributes:
| Attribute | Example | Description |
|---|---|---|
.status_code |
200 |
HTTP status code |
.headers |
{'content-type': 'application/json'} |
Case-insensitive dict of response headers |
.text |
'{"key": "value"}' |
Raw response body as string |
.json() |
{'key': 'value'} |
Parsed JSON (raises httpx.DecodingError if invalid) |
.content |
b'{"key": "value"}' |
Response body as bytes |
.raise_for_status() |
raises HTTPStatusError if status >= 400 |
Graceful error handling |
5. Async variant
For async code, replace httpx.Client with httpx.AsyncClient:
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/items")
data = response.json()
Hands-on walkthrough
Let's put this into practice with three complete examples.
Example 1: Fetch and display data from a public API
We'll use the public Dog API to get a random dog image.
import httpx
def get_random_dog_image() -> str:
"""Fetch a random dog image URL from Dog API."""
url = "https://dog.ceo/api/breeds/image/random"
response = httpx.get(url)
if response.status_code == 200:
data = response.json()
return data["message"]
else:
return f"Error: {response.status_code}"
print(get_random_dog_image())
Expected output:
https://images.dog.ceo/breeds/hound-afghan/nigel_21543.jpg
Example 2: POST data with JSON and authentication
Assume we have a task management API. We'll create a new task using an API token.
import httpx
with httpx.Client(base_url="https://api.example.com", headers={
"Authorization": "Bearer your_secret_token_here",
"User-Agent": "MyPythonApp/1.0"
}) as client:
task_data = {
"title": "Learn httpx",
"description": "Consume REST APIs with httpx",
"due_date": "2025-12-31"
}
response = client.post("/tasks", json=task_data)
if response.is_success:
new_task = response.json()
print(f"Created task {new_task.get('id')}: {new_task.get('title')}")
else:
print(f"Failed: {response.status_code} - {response.text}")
Expected output (if API returns success):
Created task 42: Learn httpx
Example 3: Async batch fetching with retries
Suppose we need to fetch user profiles for a list of user IDs concurrently. This is where httpx shines.
import asyncio
import httpx
USER_IDS = [1, 2, 3, 4, 5]
API_BASE = "https://jsonplaceholder.typicode.com"
async def fetch_user(client: httpx.AsyncClient, user_id: int) -> dict:
"""Fetch a single user, retrying once on failure."""
for attempt in range(2):
try:
response = await client.get(f"/users/{user_id}")
response.raise_for_status()
return response.json()
except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
if attempt == 1:
raise exc
await asyncio.sleep(1)
async def main():
async with httpx.AsyncClient(base_url=API_BASE) as client:
tasks = [fetch_user(client, uid) for uid in USER_IDS]
users = await asyncio.gather(*tasks, return_exceptions=True)
for user_id, user in zip(USER_IDS, users):
if isinstance(user, Exception):
print(f"User {user_id}: Failed -> {user}")
else:
print(f"User {user_id}: {user['name']} - {user['email']}")
await main()
Expected output (simplified):
User 1: Leanne Graham - Sincere@april.biz
User 2: Ervin Howell - Shanna@melissa.tv
...
This async pattern gives you massive speed improvements when fetching many endpoints, without blocking other operations.
Compare options / when to choose what
| Criteria | httpx |
requests |
aiohttp |
|---|---|---|---|
| Async support | ✅ Built-in (async/await) |
❌ Requires aiohttp or threading |
✅ Async only |
| HTTP/2 | ✅ Supported | ❌ Not supported | ❌ Supported via aiohttp but not standard |
| Timeout granularity | ✅ Connect, read, write, pool separately | ❌ Single timeout for whole request | ✅ Similar to httpx |
| Retry with backoff | ✅ Built-in via httpx.Client(event_hooks) |
❌ Manual | ❌ Manual |
| File uploads | ✅ files parameter |
✅ Similar | ✅ FormData |
| Ease of use for beginners | ✅ Very similar to requests |
✅ The simplest | ❌ Requires understanding asyncio |
| Active maintenance | ✅ Yes (2024) | ✅ Yes (maintenance only) | ✅ Yes |
| License | BSD-3-Clause | Apache-2.0 | Apache-2.0 |
When to choose httpx: - You need async support (web scrapers, microservices, real-time dashboards). - You want a consistent API across sync and async code. - You need HTTP/2 for better performance with multiple requests. - You want built-in timeouts and retry logic.
When to stick with requests:
- You're writing a quick script that makes one or two calls.
- Your team is already deep into requests and doesn't need async.
- You're on a very old Python version (httpx requires 3.7+).
Troubleshooting & edge cases
1. httpx.ConnectError: All connection attempts failed
Cause: Server unreachable, DNS failure, or network blocked.
Fix:
try:
response = httpx.get("https://unreachable.example.com", timeout=5.0)
except httpx.ConnectError as e:
print(f"Network error: {e}")
2. httpx.TimeoutException: The read operation timed out
Cause: The server is too slow to respond within the default timeout (5s).
Fix: Increase timeout or configure granular timeouts:
client = httpx.Client(timeout=httpx.Timeout(10.0, connect=5.0, read=15.0))
3. typError: JSONDecodeError when using .json()
Cause: The response body is not valid JSON (e.g., HTML error page).
Fix: Always check the status code first, or use .text to inspect:
response = httpx.get("https://api.example.com/data")
if response.status_code != 200:
print(f"Unexpected response: {response.text[:200]}")
else:
data = response.json()
4. SSL errors with self-signed certificates
Cause: Internal APIs with custom or self-signed certificates.
Fix:
# For development only (do not use in production)
import httpx
client = httpx.Client(verify=False)
# Or provide a custom certificate bundle
client = httpx.Client(verify="/path/to/custom-ca-bundle.crt")
Pro tip: Use
retrieswithhttpx.Client(event_hooks={'request': [some_hook]})for automatic retry logic. See httpx documentation.
What you learned & what's next
You now understand how to consume REST APIs with httpx in Python:
- ✅ The core concept of httpx.Client as a session manager
- ✅ How to make GET and POST requests with parameters, JSON, and authentication
- ✅ The difference between sync and async APIs
- ✅ How to handle timeouts, errors, and SSL issues
- ✅ When to choose httpx over requests or aiohttp
These skills are essential for integrating third-party services, building web scrapers, or creating API-driven applications.
Next step: In the next lesson, you'll learn how to build a CLI tool that consumes multiple REST APIs using httpx, adding command-line argument parsing and error handling. This will solidify your async API consumption skills in a real-world project.
Ready to level up? Let's go!
Practice recap
Take the async example from this lesson and modify it to fetch 20 users in parallel from the same API (/users/1 through /users/20). Add a timeout=3.0 parameter to each request. Then, print the total time taken using time.perf_counter(). This exercise solidifies async consumption and timeout handling.
Common mistakes
- Forgetting to call
.json()on the response and working with the raw.textstring, leading to "TypeError: string indices must be integers" when trying to access dictionary keys. - Not using
raise_for_status()and assuming a 2xx response. API returns 404 or 500, and the code silently fails or crashes later with unexpected data. - Using a global
httpxtop-level function (likehttpx.get()) inside a loop, which creates a new connection each time instead of reusing the connection pool via aClient. - Ignoring timeouts entirely; default timeout is 5 seconds, but for slow APIs this causes
TimeoutException— set explicit timeouts for each request.
Variations
- Use
httpx.AsyncClientfor async endpoints —async with httpx.AsyncClient() as client:andawait client.get(...). - Use
httpx.Client(base_url="...")to avoid repeating the hostname in every request. - Leverage the
event_hookson Client to implement logging, retries, or custom authentication flows without modifying each request call.
Real-world use cases
- A microservice that fetches user profiles from a centralized authorization service — using async httpx to avoid blocking while awaiting other services.
- A data pipeline that calls a paginated REST API (e.g., GitHub API) to collect repository metadata — using
Clientwithparamsand connection reuse to stay within rate limits. - A chatbot that integrates with Slack API to post messages and retrieve channel history — using httpx's JSON and authentication features for seamless integration.
Key takeaways
- httpx provides both sync and async clients with a familiar
requests-like API, making it easy to consume REST APIs with httpx. - Always use
httpx.Client(orAsyncClient) to reuse TCP connections and reduce latency for multiple requests. - Handle errors explicitly using
raise_for_status()or custom checks; never assume a 200 response. - Set explicit timeouts (
httpx.Timeout(10.0)) to prevent hanging on slow servers. - Use
httpx.AsyncClientwithasyncio.gatherfor concurrent API calls — a huge performance boost for I/O-bound tasks.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.