Cooperative concurrency via async def coroutines and await / yield. No preemption, a coroutine runs until it yields, sleeps, awaits, or returns. Single-threaded scheduler; concurrency by interleaving, not parallelism.
No asyncio module. Primitives, run, sleep, frame, gather, with_timeout, cancel, receive, are top-level builtins.
import asyncio # ModuleNotFoundError: there is no asyncio# Idiomatic edge-python: call the primitives directly.
async def main():
sleep(0.01)
return "ok"
print(run(main()))okTwo kinds of callables
A def body executes immediately. An async def body returns a coroutine value that does nothing until driven with run / gather. Only coroutines are cancellable (cancel) and can suspend on real time (sleep).
A plain def inside a coroutine (or at module top-level) can still call yielding builtins (sleep, receive, deferred host calls), the scheduler snapshots the helper’s frame, suspends the call chain, re-enters the helper on resume so its return value lands at the original call site. The module body runs as an implicit coroutine, so top-level statements suspend the same way. From the caller, a sync helper that internally sleeps is indistinguishable from one that doesn’t.
def routine():
return 1
async def coro():
return 1
print(routine()) # 1
print(coro()) # <coroutine> (does not run yet)
print(run(coro())) # 1 (run drives it to completion)Driving coroutines
run(coro) executes a single coroutine to completion and returns its value.
async def square(n):
return n * n
print(run(square(5)))25run(c1, c2, ...) accepts multiple coroutines. They run concurrently; the call returns the first argument’s result.
Sleeping
sleep(seconds) suspends until seconds of wall time pass. Without a host time hook, a virtual clock advances logically, coroutines interleave deterministically with no real wait (useful for tests).
async def task(name):
print(f"{name} step 1")
sleep(0) # yield to the scheduler
print(f"{name} step 2")
run(task("a"), task("b"))a step 1
b step 1
a step 2
b step 2gather
gather(*coros) runs each concurrently, returns a list of results in argument order. If any raises, the first error (in argument order) propagates after all peers terminate. Survivors not auto-cancelled.
async def fetch(name, delay):
sleep(delay)
return name + "!"
print(gather(fetch("a", 0.05), fetch("b", 0.02), fetch("c", 0.03)))['a!', 'b!', 'c!']The total wall time is max(delays), not the sum, b and c overlap with a’s sleep.
Errors
async def good(): return 1
async def bad(): raise ValueError
try:
gather(good(), bad())
except ValueError:
print("caught")caughtConcurrent host calls
Deferred host calls (e.g. network.fetch) run concurrently under gather: each parks its coroutine, the host resolves them in parallel, and every result is routed back to the exact coroutine that issued it. A failed call raises only in its own coroutine, so a try/except lets the rest of the batch finish.
from network import fetch_text
async def status(url):
try:
fetch_text(url)
return "ok"
except:
return "failed"
# The bad URL raises inside its own coroutine; the others still resolve.
print(gather(status("https://example.com/a"), status("https://nope.invalid/x")))['ok', 'failed']with_timeout
with_timeout(seconds, coro) runs coro or raises TimeoutError on deadline; coro cancelled on timeout.
async def slow():
sleep(10)
return "never"
try:
with_timeout(0.1, slow())
except TimeoutError:
print("timed out")timed outwith_timeout evaluates the coroutine eagerly, it’s a call, not an awaitable.
cancel
cancel(coro) flags a registered coroutine for cancellation. On its next scheduler tick it transitions to Cancelled and stops. The body does not observe a CancelledError, cancellation is cooperative and silent.
A coroutine in a tight synchronous loop without await/sleep cannot be cancelled until it yields:
async def loop_forever():
for i in range(1_000_000):
pass # no yield, not cancellable here
sleep(0) # cancellable from this point onFor deadline-driven cancellation use with_timeout.
Exception types
| Exception | When |
|---|---|
TimeoutError | with_timeout deadline expired |
CancelledError | reserved for user-thrown; not auto-raised by cancel() |
Both live in the built-in exception namespace and match except clauses normally.
Limitations
- No preemption,
while True: passinside a coroutine blocks the scheduler. - Silent cancellation,
cancel(coro)stops the coro; the body doesn’t seeCancelledError. Usewith_timeoutfor deadline-as-exception. - Cooperative host loop, scheduler suspends to the host when it can’t progress synchronously (pending timer/frame/event); embedder resumes via
run_start/run_resume/run_push_event. The legacy non-suspendingruncannot resume, code usingsleep(n>0),frame(), or an emptyreceive()must run via the driver loop; statements after a top-levelrun()don’t execute after a yield. async forworks against anyfor-iterable plus coroutines and async generators (async defwithyield). Each iteration resumes to the next yield. No__aiter__/__anext__dispatch on user classes, write anasync defgenerator. Behaviour over lists/tuples/dicts is identical to regularfor.async withreuses sync dispatch (__enter__/__exit__);__aenter__/__aexit__aren’t consulted. For async setup/teardown, usetry/finallywith explicitawait.- No async comprehensions,
[x async for x in it]unsupported. - No
gen.send/throw/close, generators and coroutines are one-way producers. For bidirectional flow, userun/gatherand pass messages via args. receive()blocks indefinitely, empty queue + norun_push_eventleaves the coro parked inWaitingEvent. Pair withwith_timeoutfor a deadline.
Time capability
Scheduler reads from vm.time_hook. WASM hosts wire it to Date.now() * 1e6 via the host_now_ns import; native hosts use std::time::Instant. Without a hook, sleep advances a virtual clock so deterministic tests interleave correctly.