Python's asyncio (standard library) provides asynchronous, single-threaded concurrency — ideal for I/O-bound work like network requests, where a program spends most of its time waiting rather than computing.
import asyncio
async def fetch_data(delay, name): # 'async def' defines a coroutine
print(f"Starting {name}")
await asyncio.sleep(delay) # pauses THIS coroutine, doesn't block others
print(f"Finished {name}")
return f"{name} result"
async def main():
# Sequential — takes 3+2 = 5 seconds total
# await fetch_data(3, "A"); await fetch_data(2, "B")
# Concurrent — takes only ~3 seconds (the longest one), both run "at once"
results = await asyncio.gather(
fetch_data(3, "A"),
fetch_data(2, "B"),
)
print(results)
asyncio.run(main()) # the standard entry point for running async code
async def defines a coroutine function — calling it doesn't run the code immediately, it returns a coroutine object that must be awaited (or scheduled some other way) to actually execute. await pauses the current coroutine at an I/O operation (like asyncio.sleep, a network call, or a database query) and hands control back to the event loop, which can run other pending coroutines during that wait — this is how a single thread manages many concurrent operations without ever truly running them in parallel.
asyncio.gather(...) runs multiple coroutines concurrently, waiting for all of them to complete and returning their results together — critically, this makes independent I/O-bound tasks finish in roughly the time of the slowest one, rather than the sum of all of them, since they overlap while waiting.
asyncio shines specifically for I/O-bound workloads (network requests, file access, database queries) where the program is mostly waiting — it does not help with CPU-bound work (heavy computation), since it's still fundamentally single-threaded; for that, see multiprocessing in the next lesson.