Skip to main content

Documentation Index

Fetch the complete documentation index at: https://edgepython.com/llms.txt

Use this file to discover all available pages before exploring further.

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()))
Output
ok

Two 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)))
Output
25
run(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"))
Output
a step 1
b step 1
a step 2
b step 2

gather

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)))
Output
['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")
Output
caught

Concurrent 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")))
Output
['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")
Output
timed out
with_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 on
For deadline-driven cancellation use with_timeout.

Exception types

ExceptionWhen
TimeoutErrorwith_timeout deadline expired
CancelledErrorreserved 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: pass inside a coroutine blocks the scheduler.
  • Silent cancellation, cancel(coro) stops the coro; the body doesn’t see CancelledError. Use with_timeout for 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-suspending run cannot resume, code using sleep(n>0), frame(), or an empty receive() must run via the driver loop; statements after a top-level run() don’t execute after a yield.
  • async for works against any for-iterable plus coroutines and async generators (async def with yield). Each iteration resumes to the next yield. No __aiter__ / __anext__ dispatch on user classes, write an async def generator. Behaviour over lists/tuples/dicts is identical to regular for.
  • async with reuses sync dispatch (__enter__ / __exit__); __aenter__ / __aexit__ aren’t consulted. For async setup/teardown, use try / finally with explicit await.
  • 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, use run / gather and pass messages via args.
  • receive() blocks indefinitely, empty queue + no run_push_event leaves the coro parked in WaitingEvent. Pair with with_timeout for 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.