Skip to content

go → spawn registry

go f(x) spawns a task in the background. Lowers through typhon_runtime.tasks.spawn, never a bare asyncio.create_task.

Async context

async def signup(email: str) -> User:
let user: User = await create_user(email)
go send_welcome(user)
return user

With handle

async def signup(email: str) -> User:
let user: User = await create_user(email)
go send_welcome(user) -> email_task
await email_task
return user

The runtime helper

build/typhon_runtime/tasks.py
import asyncio
_BACKGROUND: set[asyncio.Task] = set()
def spawn(coro):
task = asyncio.create_task(coro)
_BACKGROUND.add(task)
task.add_done_callback(_BACKGROUND.discard)
return task

10 lines, generated into your build/ tree. Solves the well-known footgun: Python’s event loop holds only weak references to tasks. A fire-and-forget asyncio.create_task(...) whose handle is dropped can be garbage-collected mid-flight. The _BACKGROUND set holds strong references; the done-callback clears entries on completion.

Free-threaded variant

go always lowers through typhon_runtime.tasks.spawn (an asyncio task held in a strong-ref registry) — there is no ThreadPoolExecutor lowering for go, including when [python] free-threaded = true.

Why never asyncio.create_task directly?

This is a well-known CPython gotcha — see the asyncio docs for the warning. Tasks are weakly referenced by the loop. Without an external strong reference, the GC may collect the task and silently cancel the coroutine.

The runtime helper takes one line of overhead and prevents the entire bug class.

Where next