Fundamentals 15 min read

A Comparative Overview of Python and JavaScript Coroutines

This article compares the evolution, concepts, and practical implementations of coroutines in Python and JavaScript, explaining their histories, core mechanisms such as async/await, event loops, and providing side‑by‑side code examples to help newcomers understand asynchronous programming in both languages.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
A Comparative Overview of Python and JavaScript Coroutines

Preface

Previously I had little exposure to front‑end development and did not understand JavaScript asynchronous operations; after learning a bit, I discovered that the development histories of Python and JavaScript coroutines are almost identical.

This article provides a horizontal comparison and summary to help newcomers interested in both languages understand and absorb the concepts.

Common Requirements

With multi‑core CPUs, concurrent functionality is needed despite historical single‑threaded environments.

Simplify code and avoid callback hell; keyword support is desired.

Efficiently use OS resources and hardware: coroutines consume fewer resources and have faster context switches than threads.

What is a Coroutine?

In short, a coroutine is a function that satisfies the following conditions:

It can pause execution (the pause point is called a suspension point).

It can resume from the suspension point, preserving its original arguments and local variables.

An event loop is the underlying foundation of asynchronous programming.

Chaotic History

Python Coroutine Evolution

Python 2.2 introduced generators for the first time.

Python 2.5 added the yield keyword to the syntax.

Python 3.4 introduced yield from (approximately yield + exception handling + send ) and the experimental async I/O framework asyncio (PEP 3156).

Python 3.5 added async/await syntax (PEP 492).

Python 3.6 formalized the asyncio library, making the official documentation much clearer.

During the main development, many side‑track coroutine implementations such as Gevent also appeared.

<code>def foo():
    print("foo start")
    a = yield 1
    print("foo a", a)
    yield 2
    yield 3
    print("foo end")

gen = foo()
# print(gen.next())
# gen.send("a")
# print(gen.next())
# print(foo().next())
# print(foo().next())
# In Python 3.x, the old <code>g.next()</code> is renamed to <code>g.__next__()</code>, and <code>next(g)</code> works as well.
# <code>next()</code> can only accept <code>None</code> as an argument, while <code>send()</code> can pass a value to the <code>yield</code>.
print(next(gen))
print(gen.send("a"))
print(next(gen))
print(next(foo()))
print(next(foo()))

list(foo())
"""
foo start
1
foo a a
2
3
foo start
1
foo start
1
foo start
foo a None
foo end
"""
</code>

JavaScript Coroutine Evolution

Synchronous code.

Asynchronous JavaScript: callback hell.

ES6 introduced Promise/A+, generators (syntax function foo(){} * can pause, save context, and resume execution), new keyword yield enables generator functions to pause.

ES7 introduced async functions and await sugar; async declares an asynchronous function (wrapping a generator and an executor) that returns a Promise . await waits for a Promise to resolve and obtains the result.

Promises also use callbacks: then and catch receive functions that run when the Promise is fulfilled or rejected, allowing tasks to be chained.

In short, callbacks are transformed into .then().then()... , making code writing and reading more intuitive.

Generators are fundamentally coroutines.

<code>function* foo() {
    console.log("foo start")
    a = yield 1;
    console.log("foo a", a)
    yield 2;
    yield 3;
    console.log("foo end")
}

const gen = foo();
console.log(gen.next().value); // 1
// gen.send("a") // SpiderMonkey engine supports send syntax
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(foo().next().value); // 1
console.log(foo().next().value); // 1
/*
foo start
1
foo a undefined
2
3
foo start
1
foo start
1
*/
</code>

Python Coroutine Maturity

Awaitable objects can be used in await statements; there are three main types: coroutines, tasks, and Futures.

Coroutine

Coroutine function: defined with async def .

Coroutine object: the object returned by calling a coroutine function.

Legacy generator‑based coroutine.

Task (Task object)

Tasks are used to schedule coroutines “in parallel”. When a coroutine is wrapped with asyncio.create_task() , it is automatically scheduled for execution.

A Task runs a coroutine in the event loop. If a coroutine awaits a Future, the Task suspends the coroutine until the Future completes, then resumes it.

The event loop runs one Task at a time; a Task may wait on a Future while the loop runs other Tasks, callbacks, or I/O.

asyncio.Task inherits all APIs from Future except Future.set_result() and Future.set_exception() .

Future Object

Future objects bridge low‑level callback code and high‑level async/await code.

After moving away from callback‑style code, a Future is introduced to obtain the result of an asynchronous call. It encapsulates interaction with the loop; add_done_callback registers a callback that runs when result is set, propagating the value back to the coroutine.

Various Event Loops

libevent/libev : used by Gevent (greenlet + libevent/libev), widely adopted.

tornado : its own IOLOOP implementation.

picoev : used by meinheld (greenlet + picoev), lightweight but less maintained.

uvloop : a modern event loop built on libuv; asyncio can plug in custom loops that satisfy the API. Sanic framework uses it.

Example

<code>import asyncio
import time

async def exec():
    await asyncio.sleep(2)
    print('exec')

# Correct usage
async def go():
    print(time.time())
    await asyncio.gather(exec(), exec())  # schedule coroutine group
    print(time.time())

if __name__ == "__main__":
    asyncio.run(go())
</code>

JavaScript Coroutine Maturity

Promise Continued Use

A Promise is essentially a state machine representing the final completion (or failure) of an asynchronous operation and its result value. It has three states:

pending: initial state, neither fulfilled nor rejected.

fulfilled: operation completed successfully.

rejected: operation failed.

When the pending state changes, the Promise object calls the appropriate handler based on the final state.

async/await Sugar

async and await wrap generators and Promises, making asynchronous code look more like synchronous code and improving error handling, branching, exception stacks, and debugging.

JavaScript Asynchronous Execution Mechanism

All tasks run on the main thread, forming an execution stack.

Outside the main thread, there is a “task queue”. When an asynchronous task produces a result, an event is placed in this queue.

Once the execution stack is empty, the system reads the task queue, moves corresponding asynchronous tasks into the stack, and executes them.

Synchronous tasks execute directly; asynchronous tasks are classified as macro‑tasks and micro‑tasks.

When the execution stack finishes, the engine first processes all micro‑tasks, then picks one macro‑task. Within a single event loop, micro‑tasks always run before macro‑tasks.

Example

<code>var sleep = function (time) {
    console.log("sleep start")
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve();
        }, time);
    });
};

async function exec() {
    await sleep(2000);
    console.log("sleep end")
}

async function go() {
    console.log(Date.now())
    c1 = exec()
    console.log("-------1")
    c2 = exec()
    console.log(c1, c2)
    await c1;
    console.log("-------2")
    await c2;
    console.log(c1, c2)
    console.log(Date.now())
}

go();
</code>

Event Loop Task Classification

The main thread loop reads events from the “task queue”.

Macro‑tasks (e.g., setTimeout , setInterval , XHR, I/O, UI rendering) are executed synchronously by the browser and participate in the event loop.

Micro‑tasks (e.g., Promise, process.nextTick in Node, MutationObserver ) run directly in the JavaScript engine without involving the event loop.

Summary and Comparison

Aspect

Python

JavaScript

Comment

Process

Single‑process

Single‑process

Consistent

Yield/Resume

yield, yield from, next, send

yield, next

Similar, but JavaScript rarely needs

send

Future Object (Callback Wrapper)

Futures

Promise

Both solve callback hell with similar ideas

Generator

generator

Generator

Wrap

yield

into coroutine, same concept

Mature Keywords

async, await

async, await

Keyword support identical

Event Loop

Core of asyncio; runs async tasks, callbacks, network I/O, subprocesses; rich API, high controllability.

Browser‑based, largely a black box; tasks have priority classification and different scheduling.

Large differences due to runtime environments; Python’s loop is more comparable to Node.js’s.

That concludes the article. Feel free to comment if there are any mistakes.

javascriptasynccoroutineEvent Loopawait
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.