Python Coroutine vs Task: Key Differences
python coroutine vs task: Understand the difference between Python coroutines and asyncio Tasks, how to create and await them, and when to use each for concurrent code.
When writing concurrent code with Python's asyncio, the distinction between a coroutine and a Task is central. A coroutine is a function defined with async def that can be paused and resumed. A Task is a scheduled coroutine that runs on the event loop. Understanding python coroutine vs task determines how you structure your async code.
What Is a Coroutine in Python?
A coroutine is a function defined with async def. When you call it, you get a coroutine object, but the function body does not run until the coroutine is awaited or scheduled. For example:
async def fetch_data(): return 42
Calling fetch_data() returns a coroutine object. To execute it, you must await it:
result = await fetch_data()
A coroutine can also be scheduled as a Task, which we'll cover next.
What Is an asyncio Task?
A Task is a wrapper around a coroutine that schedules it on the event loop. When you create a Task, the coroutine is queued for execution and runs concurrently with other tasks. You create a Task with asyncio.create_task():
import asyncio async def main(): task = asyncio.create_task(fetch_data()) await task
The Task runs independently, and you can await it later. Tasks are the primary way to run multiple coroutines concurrently.
Creating and Awaiting a Task
To create a Task, you pass a coroutine to asyncio.create_task(). This requires a running event loop. Inside an async def function, you typically have one. For example:
import asyncio async def say_hello(): await asyncio.sleep(1) print("Hello") async def main(): task = asyncio.create_task(say_hello()) # Do other work await task asyncio.run(main())
The task starts executing as soon as the event loop gets a chance. You can also use asyncio.ensure_future() but create_task() is the recommended way in modern Python.
Coroutine vs Task: Key Differences
| Feature | Coroutine | Task |
|---|---|---|
| Definition | async def function | Wrapper around a coroutine |
| Execution | Runs only when awaited | Scheduled on event loop, runs concurrently |
| Creation | Call function | asyncio.create_task(coro) |
| Awaiting | await coro | await task |
| Cancellation | Not directly cancellable | task.cancel() |
| Multiple | Sequential if awaited one by one | Can run concurrently |
The critical difference is that a coroutine object does not run until it is awaited, while a Task starts running as soon as the event loop can schedule it, even if you don't await it immediately.
When to Use a Coroutine Directly vs Wrapping It in a Task
Use a coroutine directly when you need to run it sequentially and its result is required immediately. For example, when you need to fetch data and then process it before moving on, await the coroutine.
Use a Task when you want to run a coroutine concurrently with other work. For instance, if you need to fetch two resources independently, you can create tasks for both and await them together:
async def main(): task1 = asyncio.create_task(fetch_data(1)) task2 = asyncio.create_task(fetch_data(2)) result1 = await task1 result2 = await task2
This runs both fetches concurrently, reducing total time compared to awaiting each sequentially.
Cancellation and Error Handling Differences
A coroutine that is awaited can be cancelled if the surrounding task is cancelled, but you cannot directly cancel a coroutine object. A Task has a cancel() method that raises CancelledError inside the coroutine. You can handle it with try/except:
async def my_task(): try: await asyncio.sleep(10) except asyncio.CancelledError: print("Cancelled") task = asyncio.create_task(my_task()) task.cancel()
Error handling also differs: if a Task raises an exception, it is stored in the Task and can be retrieved when you await it. If you never await the Task, the exception may be logged as "Task exception was never retrieved."
Performance and Overhead Considerations
Creating a Task has overhead because it involves scheduling and maintaining a separate object. For a small number of concurrent operations, this is negligible. But if you create thousands of tasks, you may see increased memory and scheduling overhead. In such cases, consider using asyncio.gather() or asyncio.Semaphore to limit concurrency.
A coroutine that is awaited directly does not incur this overhead, but it runs sequentially. The choice depends on whether you need concurrency.
Common Pitfalls and How to Avoid Them
One common mistake is forgetting to await a coroutine, which results in a warning and the coroutine never running. Another is creating tasks without keeping references, which can lead to them being garbage collected. Always store the task object.
Another pitfall is mixing await and create_task incorrectly. If you create a task and then immediately await it, you lose the concurrency benefit. Instead, create all tasks first, then await them.
Also, be careful with asyncio.run(): it creates a new event loop each time, so you cannot create tasks outside of an async function.
These are the key points to keep in mind when deciding between a coroutine and a Task in your asyncio code.