# How Monty Implements Async/Await Support: A Deep Dive into the Python Runtime

> Discover how Monty implements async/await support using a three-layer architecture for syntax parsing, bytecode generation, and an async executor managing coroutines and futures.

- Repository: [Pydantic/monty](https://github.com/pydantic/monty)
- Tags: deep-dive
- Published: 2026-02-16

---

**Monty implements async/await support through a three-layer architecture spanning syntax parsing, bytecode generation, and a specialized async executor that manages coroutines, external futures, and gather operations within the VM.**

Monty is Pydantic's Rust-based Python implementation designed for high-performance async execution. Understanding how Monty implements async/await support requires examining its tightly integrated compiler and runtime layers, from the initial parsing of `await` expressions to the VM's task scheduling logic.

## Syntax and Bytecode Layer

The journey of an `await` expression begins in the compiler frontend. When the parser encounters an await expression, it creates an `Expr::Await(Box<ExprLoc>)` node in [`crates/monty/src/prepare.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/prepare.rs). This representation wraps the inner expression that produces the awaitable object.

The bytecode compiler in [`crates/monty/src/bytecode/compiler.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/compiler.rs) handles this node by first compiling the inner expression, then emitting `Opcode::Await`. This opcode definition lives in [`crates/monty/src/bytecode/op.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/op.rs). When the VM executes this opcode, it pops the top-of-stack value, which must be a valid awaitable, and initiates the suspension logic.

The runtime result of an await operation is defined by the `AwaitResult` enum in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs). This enum distinguishes between several outcomes: pushing a new frame for a coroutine, yielding to the host for external futures, or returning a ready value.

## Runtime Async Types

Monty represents async operations through specialized heap-allocated types defined in [`crates/monty/src/asyncio.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/asyncio.rs). These types bridge the gap between Python-level async objects and the VM's execution engine.

**Coroutine** objects represent Python coroutines created by calling `async def` functions. The `Coroutine` struct stores the function ID, bound arguments, captured closure cells, and execution state (`New`, `Running`, or `Completed`). These are allocated on the Monty heap as `HeapData::Coroutine`.

**ExternalFuture** objects wrap async operations provided by the host environment. These appear as `Value::ExternalFuture(CallId)` where `CallId` is a monotonically increasing identifier allocated by the scheduler. This design allows Monty to await Rust async functions from Python code without blocking the VM.

**GatherFuture** objects implement `asyncio.gather(*awaitables)` semantics. The `GatherFuture` struct contains a list of `GatherItem` entries (either coroutine heap IDs or external future call IDs), task IDs for spawned coroutines, a results vector, the waiting task ID, and any pending external calls. This type enables concurrent execution of multiple awaitables while maintaining proper suspension semantics.

## The Async Executor

The core async logic resides in [`crates/monty/src/bytecode/vm/async_exec.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/async_exec.rs). When the VM encounters `Opcode::Await`, it invokes `exec_get_awaitable`, which acts as a dispatcher for the three awaitable types.

For **coroutines**, `await_coroutine` pushes a new frame onto the call stack and returns `AwaitResult::FramePushed`. The current task remains paused until the coroutine frame completes and returns a value via `handle_task_completion`.

For **external futures**, `await_external_future` records the current task as a waiter for the specific `CallId` and returns `AwaitResult::Yield(pending_calls)`. This yields control to the host, which must resolve the pending async operations.

For **gather operations**, `await_gather_future` performs three critical actions: it spawns a new `TaskId` for each coroutine item in the gather list, aggregates any external `CallId`s into `pending_calls`, and returns `AwaitResult::Yield(pending_calls)`. This allows the host to resolve external futures while the VM concurrently executes the spawned coroutine tasks.

When external futures resolve, the host calls `FutureSnapshot::resume` with the results. The VM performs an O(1) lookup via `pending_calls` to match each `CallId` to its awaiting task, stores the result in the appropriate `GatherFuture.results` slot, and either returns `AwaitResult::ValueReady` when all items complete or yields again with remaining pending calls.

## Error Propagation

Monty's async implementation handles errors through the `ExternalResult` type. When a host-resolved future returns `ExternalResult::Error(MontyException(...))`, the VM immediately converts this into a Python exception that propagates up the call stack through normal `try/except` handling.

For `gather` operations, the first error encountered cancels all other pending tasks. The error is immediately returned as the result of the `await gather(...)` expression, following Python's `asyncio.gather` semantics with `return_exceptions=False`.

## Host Integration and Python Bindings

Monty bridges async execution with host Python code through the `monty-python` crate. External async functions are declared in the `external_functions` list when creating a `MontyRun` instance. The host receives `CallId` values via the `AwaitResult::Yield` path, executes the corresponding Rust async code, and later calls `FutureSnapshot::resume` with the results.

The Rust-side bridge lives in [`crates/monty-python/src/convert.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/convert.rs) and [`crates/monty-python/src/external.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs). This architecture allows Monty to execute Python async code while delegating I/O-bound operations to the host's async runtime (such as Tokio) without blocking the VM's execution loop.

## Practical Code Examples

### Concurrent Execution with `asyncio.gather`

```python
import asyncio

async def foo():
    return 10

async def bar():
    return 32

async def main():
    a, b = await asyncio.gather(foo(), bar())
    return a + b

await main()  # → 42

```

During execution, `foo()` and `bar()` produce awaitable objects. The `asyncio.gather` call creates a `GatherFuture` containing these awaitables. The VM yields control to the host to resolve any external futures while concurrently executing the coroutines, eventually assembling the result list `[10, 32]`.

### Awaiting External Host Functions

```python
async def fetch():
    return await http_get('https://example.com')  # host-provided async function

result = await fetch()

```

Here, `http_get` returns an `ExternalFuture` with a unique `CallId`. When the VM executes the `await` opcode, it yields this ID to the host. The host performs the HTTP request asynchronously, then calls `FutureSnapshot::resume` with the result, allowing the VM to continue execution with the fetched data.

### Error Handling in Async Code

```python
async def failing_task():
    raise ValueError('boom')

try:
    await failing_task()
except ValueError as e:
    print(e)  # prints "boom"

```

When the host resolves the external call with an error result, the VM converts the `ExternalResult::Error` into a Python `ValueError` that propagates through the normal exception handling mechanism.

## Summary

- Monty implements async/await support through a three-layer architecture spanning syntax parsing, bytecode generation, and VM execution.
- The parser creates `Expr::Await` nodes in [`prepare.rs`](https://github.com/pydantic/monty/blob/main/prepare.rs), while the compiler emits `Opcode::Await` for the VM to execute.
- Runtime types including `Coroutine`, `ExternalFuture`, and `GatherFuture` in [`asyncio.rs`](https://github.com/pydantic/monty/blob/main/asyncio.rs) represent different awaitable objects on the Monty heap.
- The async executor in [`async_exec.rs`](https://github.com/pydantic/monty/blob/main/async_exec.rs) handles suspension logic, spawning tasks for coroutines, yielding to the host for external futures, and managing concurrent gather operations.
- Error propagation converts host-side `ExternalResult::Error` values into Python exceptions that traverse the normal try/except stack.
- Host integration through `monty-python` allows Monty to delegate I/O operations to external async runtimes while maintaining its own VM execution loop.

## Frequently Asked Questions

### How does Monty handle the `await` keyword at the bytecode level?

Monty compiles `await` expressions into the `Opcode::Await` instruction defined in [`crates/monty/src/bytecode/op.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/op.rs). When the VM executes this opcode, it calls `exec_get_awaitable` in [`crates/monty/src/bytecode/vm/async_exec.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/async_exec.rs), which identifies whether the awaitable is a coroutine, external future, or gather object, then dispatches to the appropriate handler to manage suspension and task scheduling.

### What is the difference between a `Coroutine` and an `ExternalFuture` in Monty?

A `Coroutine` represents a Python async function written in Monty, stored as `HeapData::Coroutine` in [`crates/monty/src/asyncio.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/asyncio.rs), containing the function ID, arguments, and execution state. An `ExternalFuture` (represented as `Value::ExternalFuture(CallId)`) wraps async operations provided by the host environment, such as I/O operations implemented in Rust, allowing Monty to await external async functions without blocking the VM.

### How does `asyncio.gather` work internally in Monty?

The `asyncio.gather` implementation creates a `GatherFuture` object that stores a list of `GatherItem` entries representing each awaitable. When awaited, the VM's `await_gather_future` function spawns separate tasks for each coroutine while aggregating external call IDs. The VM yields to the host with pending external calls, allowing concurrent execution. As results arrive via `FutureSnapshot::resume`, the VM populates the results vector, returning the final list once all tasks complete or propagating the first error encountered.

### How does Monty propagate errors from async operations?

When a host-resolved external future returns `ExternalResult::Error` containing a `MontyException`, the VM immediately converts this into a Python exception that propagates through the normal `try/except` handling mechanism. For `gather` operations, the first error cancels all other pending tasks and immediately returns as the result of the await expression, matching standard Python `asyncio.gather` behavior with `return_exceptions=False`.