Working with Async HTTP Requests: `aiohttp`
The most common use case for asyncio is to handle I/O-bound tasks, and the most common I/O task in modern software is making network requests. However, the popular requests library is synchronous. If you use it in an async function, it will block the entire event loop, defeating the purpose of asyncio.
To perform HTTP requests asynchronously, you need a library designed for it. The most popular and robust library for this is aiohttp. It provides an asynchronous HTTP client and server, allowing you to make many network requests concurrently without blocking.
📚 Prerequisites
You should be comfortable with asyncio fundamentals, including async/await and running concurrent tasks with asyncio.gather().
🎯 Article Outline: What You'll Master
In this article, you will learn:
- ✅ Installing
aiohttp: How to add the library to your environment. - ✅ The
ClientSession: Understand the importance of using a session object for making requests. - ✅ Making a GET Request: How to perform a basic
GETrequest to fetch data. - ✅ Processing the Response: How to get the status code, JSON data, and text from a response object.
- ✅ Concurrent Requests: A practical example of using
aiohttpwithasyncio.gather()to fetch multiple URLs at once.
🧠 Section 1: Installation
aiohttp is a third-party library, so you'll need to install it with pip. As always, it's highly recommended to do this in an active virtual environment.
pip install aiohttp
💻 Section 2: Making a Single Request
Making a request with aiohttp involves two main components:
aiohttp.ClientSession: This is the main entry point for all client-side requests. You should create a single session and reuse it for all requests in your application. The best way to manage it is with anasync withblock, which handles closing the session for you automatically.- The Request: Inside the
async withblock, you can use methods on thesessionobject, likesession.get(),session.post(), etc. These methods are coroutines, so you mustawaitthem.
This also returns a response object that must be used within another async with block.
Let's fetch some data from a public test API.
import aiohttp
import asyncio
async def fetch_one():
# A free public API for testing
url = "https://jsonplaceholder.typicode.com/posts/1"
print("Starting request...")
# 1. Create a client session
async with aiohttp.ClientSession() as session:
# 2. Make the GET request
async with session.get(url) as response:
# 3. The response object has methods for accessing the data
print(f"Status Code: {response.status}")
# .json() is a coroutine, so it must be awaited
data = await response.json()
print(f"Response JSON: {data}")
# .text() is also a coroutine
# text_data = await response.text()
asyncio.run(fetch_one())
Output:
Starting request...
Status Code: 200
Response JSON: {'userId': 1, 'id': 1, 'title': 'sunt aut facere ...', 'body': 'quia et suscipit...'}
Notice the nested async with blocks. This is the standard, safe pattern for using aiohttp. The outer block manages the session, and the inner block manages the response from a single request.
🛠️ Section 3: Concurrent Requests with asyncio.gather()
The real power of aiohttp is realized when you combine it with asyncio.gather() to perform many requests concurrently. This is perfect for web scraping or querying multiple API endpoints at once.
Let's write a script to fetch the titles of the first 5 posts from the JSONPlaceholder API.
import aiohttp
import asyncio
import time
async def fetch_post_title(session, post_id: int):
"""Fetches a single post and returns its title."""
url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
try:
async with session.get(url) as response:
# Raise an exception for bad status codes (4xx or 5xx)
response.raise_for_status()
data = await response.json()
return data['title']
except aiohttp.ClientError as e:
print(f"Request for post {post_id} failed: {e}")
return None
async def main():
start_time = time.time()
# Create a single session to be reused for all requests
async with aiohttp.ClientSession() as session:
# Create a list of coroutines to run
tasks = [fetch_post_title(session, i) for i in range(1, 6)]
# Run them all concurrently
titles = await asyncio.gather(*tasks)
end_time = time.time()
print(f"Fetched {len(titles)} titles in {end_time - start_time:.2f} seconds.")
print("\n--- Titles ---")
for i, title in enumerate(titles, 1):
if title:
print(f"{i}. {title[:40]}...") # Print first 40 chars
asyncio.run(main())
How it works:
- We create a single
ClientSessionin ourmainfunction. This is much more efficient than creating a new one for each request. - We pass this
sessionobject as an argument to ourfetch_post_titlecoroutine. - We create a list of tasks, where each task is a call to
fetch_post_titlewith a differentpost_id. asyncio.gather(*tasks)runs all these requests concurrently. The total time taken will be close to the time of the single slowest request, not the sum of all of them.
✨ Conclusion & Key Takeaways
aiohttp is the standard and essential tool for any asyncio application that needs to communicate over HTTP. By integrating it with asyncio.gather(), you can build incredibly fast and efficient network clients.
Let's summarize the key takeaways:
- Use an async library for async code: Standard libraries like
requestswill block the event loop. Useaiohttpinstead. - Use a single
ClientSession: Create one session and reuse it for multiple requests for better performance. - Use
async with: This pattern ensures that both the session and the response are closed properly. - Combine with
gatherfor concurrency: The true power ofaiohttpis unlocked when you run many requests concurrently withasyncio.gather().
➡️ Next Steps
Making network requests is just one part of I/O. What about reading and writing files? While standard file I/O can be blocking, there are libraries to handle this asynchronously as well. In the next article, we'll explore "Asynchronous File I/O: aiofiles."
Happy fetching!