Back to Blog
Python

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.

asynciocoroutinestasksconcurrencyevent loop
Diagram showing a coroutine function and an asyncio Task wrapping it, illustrating the difference between a coroutine object and a scheduled task.

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

FeatureCoroutineTask
Definitionasync def functionWrapper around a coroutine
ExecutionRuns only when awaitedScheduled on event loop, runs concurrently
CreationCall functionasyncio.create_task(coro)
Awaitingawait coroawait task
CancellationNot directly cancellabletask.cancel()
MultipleSequential if awaited one by oneCan 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.

python coroutine vs task: Practical Usage and Code Examples | RYUSLOG DEV