-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
211: Awaiting Multiple Coroutines with asyncio.gather
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:
- 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 (userandom.uniform), and then return a mock price (any float). - Create a list of tickers:
["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"]. - Write a
main()coroutine that usesasyncio.gatherto fetch the prices for all these tickers concurrently. Remember to unpack the list of coroutines. - Print the final list of prices and the total time taken to ensure it didn't take the sum of all sleep timers.
There are no comments for now.