Back to Blog
Python

Python Coroutine vs Task: Key Differences

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 you write concurrent code with Python's asyncio, you need to know the difference between a coroutine and a Task. A coroutine is a function defined with async def that can pause and resume. A Task is a scheduled coroutine that runs on the event loop. That distinction affects how you structure 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. Inside an async def function, you can execute it by awaiting it:

result = await fetch_data()

A coroutine can also be scheduled as a Task, which is covered next.

What Is an asyncio Task?

A Task is a wrapper around a coroutine that schedules the coroutine on the event loop. When you create a Task, the coroutine is queued for execution and can run concurrently with other tasks. You create a Task with asyncio.create_task():

import asyncio async def fetch_data(): return 42 async def main(): task = asyncio.create_task(fetch_data()) await task asyncio.run(main())

The Task starts running when the event loop gets a chance, even if you do not await it immediately.

Creating and Awaiting a Task

asyncio.create_task() requires a running event loop. In normal code, you call it inside an async def function that is running under asyncio.run() or another Task. You cannot call it at module level before a loop is running.

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 here await task asyncio.run(main())

If you need the Task's result or need to know when it finishes, await the Task. asyncio.ensure_future() can also schedule a coroutine, but asyncio.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, can run concurrently
CreationCall the functionasyncio.create_task(coro)
Awaitingawait coroawait task
CancellationNot directly cancellabletask.cancel()
Multiple operationsSequential if awaited one by oneCan run concurrently

The main difference is that a coroutine object does not run until it is awaited, while a Task is scheduled on the event loop as soon as it is created. If you do not await the Task immediately, it can still make progress when the event loop runs.

When to Use a Coroutine Directly vs Wrapping It in a Task

Use a coroutine directly when you want to run it sequentially and must have its result before continuing. For example, inside an async def function:

value = await fetch_data() process(value)

Use a Task when you want a coroutine to run concurrently with other work. For independent operations, create the Tasks first, then await them:

async def main(): task1 = asyncio.create_task(fetch_data(1)) task2 = asyncio.create_task(fetch_data(2)) result1 = await task1 result2 = await task2

If you do not need separate Task references, asyncio.gather() is a convenient alternative:

results = await asyncio.gather(fetch_data(1), fetch_data(2))

For I/O-bound operations, this lets the operations overlap while the coroutines await I/O.

Cancellation and Error Handling Differences

A coroutine object that is awaited directly cannot be cancelled by itself; cancellation normally comes from cancelling the Task that contains the await. A Task has a cancel() method that requests cancellation. The CancelledError is raised inside the coroutine at its next suspension point:

import asyncio async def my_task(): try: await asyncio.sleep(10) except asyncio.CancelledError: print('Cancelled') raise async def main(): task = asyncio.create_task(my_task()) await asyncio.sleep(0.1) task.cancel() try: await task except asyncio.CancelledError: pass asyncio.run(main())

If the coroutine re-raises CancelledError, awaiting the cancelled Task also raises CancelledError. If a Task raises any other exception, the exception is stored on the Task and is raised again when you await the Task. If you never await the Task, asyncio may log a Task exception was never retrieved warning.

Performance and Overhead Considerations

Creating a Task has a little more overhead than awaiting a coroutine directly because the event loop has to schedule the coroutine and manage the Task object. For a modest number of concurrent operations, this overhead is small.

If you need very many concurrent operations, do not create an unlimited number of Tasks without thought. Use asyncio.gather() for a known group of awaitables when you do not need to manage each Task individually. If you need to limit concurrency, use asyncio.Semaphore to control how many operations run at once.

Common Pitfalls and How to Avoid Them

One common mistake is forgetting to await a coroutine. Calling fetch_data() returns a coroutine object; if you do not await it or schedule it, the function body never runs and Python may emit a RuntimeWarning.

Another issue is creating Tasks without keeping references to them. The asyncio documentation recommends saving a reference to a Task if you need to await or cancel it later, so it does not disappear while you still need it.

Also, do not create a Task and immediately await it if you want concurrency. If there is no other work between creation and awaiting, the code behaves like a sequential await. Create all Tasks first, then await them together.

Finally, remember that asyncio.run() creates and closes a new event loop each time it is called. Call asyncio.create_task() while an event loop is running, such as inside the main coroutine you pass to asyncio.run().

Python Coroutine vs Task: Key Differences and When to Use Each | RYUSLOG DEV