Dragon

Colorless async/await

Green threads gave you fire and Task[T].join(). There's a second way to wait on a task - await - and it comes with the single most important property of Dragon's concurrency model: it never colors your functions. await can appear in any function, and using it imposes no obligation on the callers above.

await does what join does

await blocks until a task's value is ready and returns it - the same as join()

  • but it reads better at a call site that does I/O:
async def fetch_data(url: str) -> str {
    # ... network I/O, runs on a green thread ...
    return "Data"
}

def show() -> None {
    result: str = await fetch_data("https://example.com")
    print(result)               # Data
}

show()

Notice what show() is not: it is not async. This is the Dragon Rule - await is allowed in any function, not just async ones.

Why that matters: no function coloring

In Python, one async def at the bottom forces every caller above it to become async too, all the way up to asyncio.run. That's the function coloring problem: a leaf change ripples through dozens of signatures, and you end up maintaining two of everything - read and async_read, Lock and AsyncLock.

Dragon refuses that trade. The await keyword stays - so a reader can see where a function might pause - but it carries no obligation upward. The caller of show() doesn't know or care that any I/O happened. There is one Lock, one read, one of everything; concurrency is a property of how you call, not a virus in the type signature.

What async def actually does

It changes the return type. Calling an async def f() -> T gives you a Task[T], not a T - the body runs on a green thread. await is how you unwrap it:

async def fetch(url: str) -> str {
    return "..."
}

data: str = await fetch("https://x.com")   # await -> str (run and wait)
task: Task[str] = fetch("https://x.com")   # no await -> Task[str] (run, hold the handle)
later: str = await task                    # unwrap when you're ready

await only works on a Task. Awaiting a plain value, or the result of an ordinary synchronous function, is a compile error - the compiler tells you to drop the await because there's nothing to wait for.

fire vs async def

Both produce a Task[T]; they differ in intent:

fire fn()async def + await
Reads as"run this in parallel""this does I/O, pause me until it's done"
Typical useCPU fan-out, background worksequential network / disk calls
Collect witht.join()await (inline)

Reach for fire when you're launching parallel work; reach for await when you're writing a straight-line sequence that happens to touch the network or disk. They're the same machinery - a Task[T] on a green thread - wearing two names that read right in two different situations.

Methods spawn the same way: an async def method on a class returns a Task[T] when called and unwraps with await, exactly like a free function (@classmethod is the one place async is refused).

Dropping a task

A Task[T] in statement position with nobody holding it is a silent detach - the reader sees an ordinary call, the runtime sees fire-and-forget. Dragon refuses it at compile time:

# doc: no-check
post(url, payload)   # error: this expression produces a 'Task[Response]'
                     # that is silently discarded; bind it, then await,
                     # join, or del it

Every legal spelling states its intent:

# doc: no-check
r: Response = await post(url, payload)   # wait inline
t: Task[Response] = post(url, payload)   # hold the handle for later
del t                                    # explicit fire-and-forget
fire log_metric(n)                       # fire IS the explicit detach verb

A dropped-but-bound task detaches at scope exit and its unclaimed result is released when the task finishes - the result slot is an own field of the Task, moved out by await/join and freed with the task otherwise. The ownership story lives in Ownership.

At a glance

You want to...Write
A function that returns a Task[T]async def f(...) -> T { ... }
Run it and wait inlinex: T = await f(...)
Run it, hold the handlet: Task[T] = f(...) then await t
Use await in a plain functionjust do it - no async needed on the caller
Fire-and-forget a held handledel t

await and join wait on green threads. When you need real hardware parallelism instead, drop to the OS-thread tiers: Scoped and Manual OS Threads.