Python Coroutine vs Future: Key Differences
python coroutine vs future: Understand the difference between Python coroutines and futures, how they work together in asyncio, and when to use each for efficient asyn...
python coroutine vs future requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When working with asyncio, you will encounter both coroutines and futures. They are related but serve different purposes. Understanding the distinction between a Python coroutine and a future is essential for writing efficient asynchronous code. This article explains the difference, how they interact, and when to use each.
What Is a Future in Python?
A Future is a low-level awaitable object that represents an eventual result of an asynchronous operation. It is similar to JavaScript's Promise. When you create a future, it starts in a pending state. Later, you can set its result or exception, which transitions it to a finished state.
import asyncio async def main(): loop = asyncio.get_running_loop() future = loop.create_future() print(f"Future state: {future.done()}") # False # Schedule setting the result after a delay loop.call_later(1, future.set_result, "done") result = await future print(f"Result: {result}") # done asyncio.run(main())
Here, create_future() creates a future that is not done. The call_later schedules a callback that sets the result after one second. Awaiting the future suspends the coroutine until the future is resolved.
Futures are typically used internally by asyncio to wrap callbacks and integrate with lower-level I/O. You rarely need to create a future directly in application code, but you will encounter them when using APIs like asyncio.ensure_future() or loop.run_in_executor().
What Is a Coroutine in Python?
A coroutine is a function defined with async def. When called, it returns a coroutine object that must be awaited or scheduled on an event loop. Coroutines are the primary way to write asynchronous code in Python because they allow you to write sequential-looking code that suspends and resumes at await points.
async def fetch_data(): await asyncio.sleep(1) return "data" async def main(): coro = fetch_data() # This does not execute yet result = await coro # Now it runs print(result) asyncio.run(main())
The coroutine fetch_data does not run until it is awaited. Awaiting it schedules its execution on the event loop. Coroutines can await other coroutines, futures, and tasks.
How Coroutines and Futures Interact in asyncio
In asyncio, a Task is a subclass of Future that wraps a coroutine. When you schedule a coroutine using asyncio.create_task(), it becomes a task, which is a future that runs the coroutine concurrently. This is the most common way coroutines and futures interact.
import asyncio async def worker(name, delay): await asyncio.sleep(delay) return f"{name} done" async def main(): task1 = asyncio.create_task(worker("task1", 2)) task2 = asyncio.create_task(worker("task2", 1)) results = await asyncio.gather(task1, task2) print(results) # ['task1 done', 'task2 done'] asyncio.run(main())
Here, create_task() schedules the coroutines as tasks, which are futures. asyncio.gather() awaits both tasks and collects their results.
You can also await a future directly inside a coroutine, as shown in the first example. The await expression works on any awaitable, including futures, coroutines, and tasks.
Key Differences: Coroutine vs Future
The main difference is that a coroutine is a function that can suspend and resume, while a future is a handle to an asynchronous operation's result. A coroutine defines the logic; a future represents the state of an operation.
| Aspect | Coroutine | Future |
|---|---|---|
| Definition | async def function | Class asyncio.Future |
| Execution | Runs when awaited or scheduled | Holds a result that will be set later |
| State | Suspended/resumed | Pending/finished |
| Use case | Writing async logic | Wrapping callbacks, integrating with low-level I/O |
| Creation | Calling async def function | loop.create_future() or asyncio.ensure_future() |
Coroutines are the building blocks of async code. Futures are more of a plumbing mechanism. In practice, you will almost always work with coroutines and tasks (which are futures) rather than raw futures.
When to Use Coroutines vs Futures
Use coroutines for the logic of your asynchronous operations. They are readable, composable, and support async with and async for. Use futures when you need to integrate with callback-based APIs or when you need a handle to an operation that may be resolved outside the event loop.
For example, if you are using loop.run_in_executor() to run blocking code in a thread pool, it returns a future. You can await that future directly.
import asyncio import time def blocking_work(): time.sleep(1) return "blocking done" async def main(): loop = asyncio.get_running_loop() future = await loop.run_in_executor(None, blocking_work) print(future) asyncio.run(main())
Here, run_in_executor returns a future that completes when the blocking function returns. Awaiting it suspends the coroutine without blocking the event loop.
Performance and Overhead Considerations
Coroutines are lightweight; they do not require a thread per operation. Futures have minimal overhead, but creating a task (which is a future) adds a small amount of scheduling overhead. In most applications, the difference is negligible compared to I/O latency.
However, avoid creating excessive tasks for trivial operations. If you have a loop that creates thousands of tasks, the overhead of scheduling and managing them can become noticeable. Prefer using asyncio.gather or asyncio.as_completed to manage a moderate number of tasks.
Also, be aware that awaiting a future that is already done does not block. The event loop can continue processing other tasks while a future is pending. This is the core benefit of async programming.
Common Misconceptions and Pitfalls
One common mistake is confusing a coroutine object with a future. Calling an async def function does not execute it; it returns a coroutine object. If you do not await it or schedule it as a task, it will never run, and Python will emit a warning about an unawaited coroutine.
Another pitfall is using asyncio.ensure_future() when you should use asyncio.create_task(). The former is lower-level and may not use the current running loop correctly in all contexts. Prefer create_task() for scheduling coroutines.
Finally, do not call future.result() on a pending future outside of an async context; it will raise an exception. Use await or add_done_callback to handle completion.