Skip to Content
Course content

211: Awaiting Multiple Coroutines with asyncio.gather

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, we've been awaiting coroutines one by one. That works fine when your tasks are dependent on each other—like needing a user ID before you can fetch that user's posts. But in the real world, you often have several independent tasks that can happen at the same time. If you await them sequentially, you're essentially turning your asynchronous code back into synchronous code, wasting all that potential.

I've seen this happen a lot in production code: a developer uses async and await, but they're still waiting for Task A to finish completely before starting Task B, even though Task B doesn't care about Task A. Let's build a small mock "User Dashboard" fetcher to see how to solve this using asyncio.gather.

Simulating our data sources

I'll start by creating three separate coroutines. In a real app, these would be HTTP requests to different microservices, but we'll use asyncio.sleep to simulate the network lag. I'm giving them different durations so we can see exactly how the timing works out.

import asyncio
import time

async def get_user_profile():
    print("Fetching profile...")
    await asyncio.sleep(1)
    return {"username": "dev_mentor", "status": "Coding"}

async def get_user_posts():
    print("Fetching posts...")
    await asyncio.sleep(2)
    return ["Post 1", "Post 2", "Post 3"]

async def get_user_followers():
    print("Fetching followers...")
    await asyncio.sleep(1.5)
    return ["Alice", "Bob", "Charlie"]

The trap of sequential awaits

Now, here is the "naive" way to put these together. I'll just await them one after another. I'm doing this on purpose to show you why it's inefficient.

async def main_sequential():
    start = time.perf_counter()
    
    profile = await get_user_profile()
    posts = await get_user_posts()
    followers = await get_user_followers()
    
    end = time.perf_counter()
    print(f"Sequential took {end - start:.2f} seconds")
    return profile, posts, followers

# If we ran this, it would take roughly 4.5 seconds (1 + 2 + 1.5)

That's a disaster. We're just sitting there idling while the event loop does nothing but wait for one specific timer to expire. We want all three requests to be "in flight" at the same time.

Bundling tasks with asyncio.gather

This is where asyncio.gather comes in. It takes multiple awaitables and schedules them as tasks to run concurrently. It then waits for all of them to finish and returns a list of the results in the order you passed the coroutines in.

async def main_concurrent():
    start = time.perf_counter()
    
    # We schedule all three to run at once
    results = await asyncio.gather(
        get_user_profile(),
        get_user_posts(),
        get_user_followers()
    )
    
    end = time.perf_counter()
    print(f"Concurrent took {end - start:.2f} seconds")
    return results

# Now this should only take about 2 seconds (the duration of the longest task)

Wait, why is my list not unpacking?

Here's a mistake I actually made early in my career. Sometimes you have a dynamic list of tasks—maybe you're fetching data for ten different IDs—and you can't just hardcode them into the gather() call. I tried doing this:

tasks = [get_user_profile(), get_user_posts(), get_user_followers()]
results = await asyncio.gather(tasks) # This is wrong!

If you do this, Python will throw a TypeError or behave unexpectedly because asyncio.gather expects positional arguments (a sequence of awaitables), not a single list object. To fix this, you have to "unpack" the list using the asterisk * operator.

# The correct way to handle a list of coroutines:
tasks = [get_user_profile(), get_user_posts(), get_user_followers()]
results = await asyncio.gather(*tasks) 

Now the event loop sees three separate arguments instead of one list, and everything works as intended. The total execution time will be dictated by the slowest coroutine in the group, which is the gold standard for concurrent I/O.




📋 Practical Task

Exercise: Building a Concurrent Stock Price Aggregator

You are building a financial dashboard that needs to fetch the current price of several stocks simultaneously. If you fetch them one by one, the dashboard feels sluggish.

Your Task:

  1. Create an async function called fetch_stock_price(ticker). It should print "Fetching [ticker]...", sleep for a random duration between 0.5 and 2.0 seconds (use random.uniform), and then return a mock price (any float).
  2. Create a list of tickers: ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"].
  3. Write a main() coroutine that uses asyncio.gather to fetch the prices for all these tickers concurrently. Remember to unpack the list of coroutines.
  4. Print the final list of prices and the total time taken to ensure it didn't take the sum of all sleep timers.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.