Understanding Asyncio Coroutines and Tasks in Python

Mar 17, 2024 ยท 3 min read

Asynchronous programming has become increasingly popular in Python. The asyncio module provides infrastructure for writing asynchronous code using coroutines and tasks. But what exactly is the difference between coroutines and tasks? This article will explain the key distinctions.

Coroutines

A coroutine is a specialized Python function that can suspend and resume execution. Normal Python functions execute straight through and return when complete. But coroutines can pause in the middle of execution and let other coroutines run, then later pick up where they left off.

Here is a simple coroutine example:

import asyncio

async def my_coroutine():
    print('My coroutine is running')
    await asyncio.sleep(1)
    print('My coroutine is done')

The async def syntax defines a coroutine. Inside the coroutine, await can be used to pause execution. In the above case we await on the asyncio.sleep() method which suspends execution for a time.

So coroutines provide the components for asynchronous, non-blocking code. But on their own they don't run. For that we need tasks.

Tasks

While coroutines define asynchronous behavior, tasks are what actually run the coroutines. We can think of a task as a wrapper around a coroutine which manages the execution.

Creating a task from a coroutine is simple:

import asyncio

async def my_coroutine():
    # coroutine code

my_task = asyncio.create_task(my_coroutine())

The event loop can then run Python code concurrently by switching between tasks:

import asyncio

async def coroutine_1():
    print('Coroutine 1 starts')
    await asyncio.sleep(1)
    print('Coroutine 1 ends')

async def coroutine_2(): 
    print('Coroutine 2 starts') 
    await asyncio.sleep(2)
    print('Coroutine 2 ends')

async def main():
    task1 = asyncio.create_task(coroutine_1())  
    task2 = asyncio.create_task(coroutine_2())

    await task1
    await task2

asyncio.run(main())

This concurrently runs both coroutines by creating tasks from them. The key difference from coroutines is that tasks actually execute the coroutine code.

Coroutines Define, Tasks Run

A summary of the distinction:

  • Coroutines: Define asynchronous, non-blocking behavior with async def and await. But coroutines don't actually run yet.
  • Tasks: Wrap coroutines and execute them, manage the execution flow. Tasks allow concurrency when switched by the event loop.
  • So coroutines provide the components, but tasks are what make them run.

    Here is one way to think about it:

  • Coroutines are like an asynchronous recipe
  • Tasks execute those recipes concurrently
  • The recipe defines the steps, but someone still needs to do the cooking!

    Practical Example

    Here is a practical example of using both coroutines and tasks in an application.

    Let's say we need to make multiple API calls concurrently. We define an api_call coroutine which makes an API request and processes the result:

    import asyncio
    import httpx
    
    async def api_call(url):
        print(f'Making API call to {url}')
        async with httpx.AsyncClient() as client:
            resp = await client.get(url)
            print(f'API call completed, status {resp.status_code}')
            return resp.json()

    Then we create tasks from api_call and gather the responses back once all complete:

    import asyncio
    
    async def main():
        url1 = 'https://api.domain1.com/endpoint1' 
        url2 = 'https://api.domain2.com/endpoint2'
    
        task1 = asyncio.create_task(api_call(url1))
        task2 = asyncio.create_task(api_call(url2))
    
        responses = await asyncio.gather(task1, task2)
        print(responses[0], responses[1])
    
    asyncio.run(main())

    So the api_call coroutine defines the async behavior, while the tasks actually run the coroutines and give us concurrency.

    Key Takeaways

  • Coroutines define asynchronous code with async/await
  • Tasks wrap and execute coroutines
  • Coroutines are like recipes, tasks do the cooking
  • Use coroutines to define concurrent behavior
  • Use tasks to run and manage coroutine execution
  • So in summary, coroutines provide asynchronous components while tasks are what actually run the coroutines and enable concurrency in Python. Together they allow us to make asynchronous, non-blocking applications.

    Browse by tags:

    Browse by language:

    The easiest way to do Web Scraping

    Get HTML from any page with a simple API call. We handle proxy rotation, browser identities, automatic retries, CAPTCHAs, JavaScript rendering, etc automatically for you


    Try ProxiesAPI for free

    curl "http://api.proxiesapi.com/?key=API_KEY&url=https://example.com"

    <!doctype html>
    <html>
    <head>
        <title>Example Domain</title>
        <meta charset="utf-8" />
        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
    ...

    X

    Don't leave just yet!

    Enter your email below to claim your free API key: