Challenge 2: Timed Sequential vs Concurrent Coroutines — Possible Solution ==================================================================== import asyncio import time async def download(name): print(f"{name}: starting") await asyncio.sleep(2) print(f"{name}: done") async def main(): # sequential start = time.time() for i in range(3): await download(f"file{i}") sequential_time = time.time() - start print(f"Sequential: {sequential_time:.2f}s") # concurrent start = time.time() await asyncio.gather( download("fileA"), download("fileB"), download("fileC"), ) concurrent_time = time.time() - start print(f"Concurrent: {concurrent_time:.2f}s") asyncio.run(main()) Output (timing will vary slightly, but always roughly this shape): Sequential: 6.01s Concurrent: 2.01s WHY THIS WORKS AS AN ANSWER ------------------------------ The sequential block reuses this chapter's own download(name) coroutine, called three times with await inside an ordinary for loop (Course 1, Chapter 4) — each await download(...) fully completes, including its 2-second asyncio.sleep(2), before the loop moves to the next iteration, so three calls take roughly 3 x 2 = 6 seconds total, just like Chapter 3's sequential threading challenge but using coroutines instead of plain function calls. The concurrent block reuses this chapter's own asyncio.gather() example exactly, passing three separate download(...) coroutine calls to it at once. Because each one hits await asyncio.sleep(2) and hands control back to the event loop rather than blocking, all three "waits" overlap on the SAME single thread — bringing total time down to roughly 2 seconds, directly paralleling Chapter 3's own threading-vs-sequential timing result, but achieved here with zero extra threads at all. time.time() before and after each block reuses the same before/after timing pattern from Chapter 3's own threading challenge, applied here to make the sequential-vs-concurrent speed difference directly visible rather than just asserted.