Making web requests is a common task in Python programming. However, traditional requests can block the execution of your code while waiting for a response. This can make your programs feel sluggish or unresponsive.
Enter asynchronous HTTP requests - a way to execute requests in a non-blocking manner. By running requests "in the background", asynchronous requests allow your Python code to continue executing while the request is pending. This leads to faster and more responsive programs.
In this post, we'll learn the basics of asynchronous requests in Python. We'll cover:
Let's dig in!
Blocking vs Asynchronous Code
To understand what asynchronous code is, let's first contrast it with standard "blocking" code.
When you make a typical function call in Python, the execution of your program "blocks" until that function returns a value. For example:
response = requests.get('https://api.example.com/users')
print(response.text)
Here our code stops and waits while
Asynchronous code works differently. Long-running operations are started, but execution continues without having to wait for them. The operations run concurrently along with the rest of our program.
For example, making an asynchronous HTTP request would start the request right away, allow other code to keep running, and handle the response whenever it comes back from the server:
start_request() # Starts immediately and returns
print("I don't have to wait!")
response = get_response() # Gets response whenever it's ready
This allows a Python program to avoid unnecessary waiting and do more work while I/O operations are in progress.
Introducing asyncio
In Python, asynchronous programming is centered around the
At the core of asyncio is an event loop. This loop runs your asynchronous code and handles switching between any pending tasks and operations. By leveraging the event loop, we can avoid threaded or multi-process code while still achieving concurrency in Python.
To use asyncio, you write coroutines - generator-like functions that use the
Let's see a simple coroutine in action:
import asyncio
async def my_coro(text):
print("Do work before await")
await asyncio.sleep(1)
print(f"Finished await: {text}")
asyncio.run(my_coro("Hello World!"))
This schedules
Pretty cool! Now let's apply this idea to make some asynchronous web requests.
Making Async HTTP Requests with aiohttp
The most popular way to perform asynchronous HTTP requests in Python is with the
To make an async GET request with aiohttp:
import aiohttp
async def fetch_data(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
html = await fetch_data(session, "http://python.org")
print(html[:100])
asyncio.run(main())
Breaking this down:
By using
Async Functions with async/await Syntax
Coroutines introduced earlier are written asynchronously but called synchronously. To call them inline without blocking, we need one more ingredient - the
Functions defined with
We
import asyncio
import aiohttp
async def fetch_page(url):
session = aiohttp.ClientSession()
response = await session.get(url)
text = await response.text()
await session.close()
return text
def main():
loop = asyncio.get_event_loop()
html = loop.run_until_complete(fetch_page("http://python.org"))
print(html[:100])
main()
Now
Async functions allow us to use the full capabilities of
Handling Responses from Async Requests
Once an asynchronous request completes, how do we work with the response and potential errors?
Awaiting the request returns the response object itself. We can check the status, headers, and other metadata on it:
async def get_user(id):
url = f'https://api.example.com/users/{id}'
try:
response = await session.get(url)
if response.status == 200:
json = await response.json()
return json['name']
else:
print(f"Error with status {response.status}")
return None
except aiohttp.ClientConnectorError:
print("Connection problem")
return None
Like synchronous code, use try/except blocks to handle any connection errors, 500 statuses, timeouts, etc. The body content or errors become available once the response is awaited.
For APIs that return JSON, await the
When Asyncio Gets Tricky
Asynchronous programming opens up performance gains through concurrency, but it also creates new potential issues:
While asyncio is powerful, these complexities make using it correctly non-trivial. Thorough testing is a must to avoid subtle concurrency issues. Consider if threads/processes might work better for your use case.
Next Steps
We've covered the basics, but there is much more to discover with asyncio:
Asyncio is a versatile framework for all kinds of asynchronous tasks. Whether for highly concurrent network apps, CPU-bound processing, or interfacing with async libraries, give asyncio a look for making Python faster.
The event loop handles all the difficult concurrency details - our code can focus on business logic while remaining performant, responsive and clear. This lets Python support demanding applications typically requiring lower-level languages.