# How Iterative Execution Works in Monty: A Complete Guide to Sandboxed Async Python

> Discover how iterative execution in Monty enables sandboxed async Python. Learn how Monty pauses at external calls and futures, serializes VM state, and resumes execution for seamless integration.

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

---

**Monty's interpreter implements iterative execution by pausing at external function calls or asynchronous futures, serializing the entire VM state into a snapshot, and resuming later once the host provides the result, enabling sandboxed, async-friendly Python execution.**

The `pydantic/monty` repository provides a sandboxed Python interpreter designed for safe embedding in JavaScript and Python environments. At the core of its architecture lies **iterative execution**, a mechanism that allows the interpreter to run incrementally, yielding control back to the host whenever it encounters external operations. This design pattern ensures that untrusted code cannot block the host runtime while maintaining full compatibility with Python's async/await semantics.

## What Is Iterative Execution in Monty?

Iterative execution refers to Monty's ability to run a Python script **incrementally**, pausing whenever it reaches an external (host-provided) function or an asynchronous future, and then resuming later with the result supplied by the host. This approach decouples the interpreter from the host's I/O or computation, allowing the host to implement security policies, rate limiting, or async event loops around the execution.

## Starting an Iterative Run with `MontyRun::start`

The entry point for iterative execution is `MontyRun::start` in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs). This method consumes a `MontyRun` configuration, initializes the heap and namespaces, creates a `VM` instance, and begins execution:

```rust
pub fn start<T: ResourceTracker>( … ) -> Result<RunProgress<T>, MontyException> {
    …
    let mut vm = VM::new(&mut heap, &mut namespaces, &executor.interns, print);
    let vm_result = vm.run_module(&executor.module_code);
    let vm_state = vm.check_snapshot(&vm_result);
    handle_vm_result(vm_result, vm_state, executor, heap, namespaces)
}

```

*Source: [`crates/monty/src/run.rs:21-34`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs#L21-L34)*

The `handle_vm_result` function (lines 64-78) inspects the `FrameExit` returned by the VM and converts it into a `RunProgress` enum that encodes why execution stopped.

## Understanding the `RunProgress` Enum

The `RunProgress` enum in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) represents the possible states when execution pauses:

```rust
pub enum RunProgress<T: ResourceTracker> {
    /// Paused at a synchronous external function call.
    FunctionCall { function_name, args, kwargs, call_id, state },

    /// Paused at an OS‑level operation (e.g. file I/O).
    OsCall { function, args, kwargs, call_id, state },

    /// All async tasks are blocked waiting for external futures.
    ResolveFutures(FutureSnapshot<T>),

    /// Execution finished.
    Complete(MontyObject),
}

```

*Source: [`crates/monty/src/run.rs:71-99`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs#L71-L99)*

### FunctionCall and OsCall Variants

The **`FunctionCall`** variant indicates the VM paused at a host-provided synchronous function. It carries a `state: Snapshot<T>` containing the entire VM, heap, and namespace state at the pause point. The **`OsCall`** variant follows the same pattern but specifically identifies OS-level operations like file I/O.

### ResolveFutures for Async Operations

When the external call is a coroutine or when Python code uses `await` on a host-provided future, the VM returns **`ResolveFutures`**. This variant wraps a `FutureSnapshot<T>` that tracks multiple pending asynchronous operations, enabling incremental resolution where the host can provide results for a subset of pending futures.

## Resuming Execution After a Pause

### Synchronous Resumption with `Snapshot::run`

To resume after a `FunctionCall` or `OsCall`, the host calls `Snapshot::run`, which restores the VM from the saved `VMSnapshot`, injects the host-provided result (or exception), and continues execution:

```rust
pub fn run(mut self, result: impl Into<ExternalResult>, print: &mut PrintWriter<'_>)
    -> Result<RunProgress<T>, MontyException> {
    let ext_result = result.into();
    let mut vm = VM::restore(self.vm_state, …);
    let vm_result = match ext_result {
        ExternalResult::Return(obj) => vm.resume(obj),
        ExternalResult::Error(exc) => vm.resume_with_exception(exc.into()),
        ExternalResult::Future => { … }
    };
    …
    handle_vm_result(vm_result, vm_state, self.executor, self.heap, self.namespaces)
}

```

*Source: [`crates/monty/src/run.rs:83-92`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs#L83-L92) and [`run.rs:121-124`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs#L121-L124)*

### Async Resumption with `FutureSnapshot::resume`

For asynchronous operations, `FutureSnapshot::resume` accepts a list of `(call_id, ExternalResult)` pairs, resolves the futures inside the scheduler, and continues execution until it hits the next pause point:

```rust
pub fn resume(self, results: Vec<(u32, ExternalResult)>, print: &mut PrintWriter<'_>)
    -> Result<RunProgress<T>, MontyException> {
    …
    // validate call_ids, restore VM, resolve each future
    // possibly return ResolveFutures again if more futures are pending
    …
    handle_vm_result(vm.run(), vm_state, executor, heap, namespaces)
}

```

*Source: [`crates/monty/src/run.rs:81-111`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs#L81-L111) and detailed logic at [`run.rs:124-154`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs#L124-L154)*

## Practical Implementation Examples

### Rust Implementation

The following example demonstrates the complete iterative execution flow in Rust, handling a synchronous external function:

```rust
use monty::{MontyRun, MontyObject, RunProgress, PrintWriter, NoLimitTracker};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1️⃣ Create a runner for a script that calls an external function `host_add`
    let runner = MontyRun::new(
        "result = host_add(a, b)".to_owned(),
        "demo.py",
        vec!["a".to_owned(), "b".to_owned()],
        vec!["host_add".to_owned()],
    )?;

    // 2️⃣ Start execution with initial inputs
    let mut out = PrintWriter::Stdout;
    let prog = runner.start(
        vec![MontyObject::Int(3), MontyObject::Int(4)],
        NoLimitTracker,
        &mut out,
    )?;

    // 3️⃣ Handle the pause – an external function call
    if let RunProgress::FunctionCall { state, .. } = prog {
        // Host implements `host_add` – simply add the two arguments
        let args = state.clone().into_function_call().unwrap().1;
        let sum = match (&args[0], &args[1]) {
            (MontyObject::Int(a), MontyObject::Int(b)) => MontyObject::Int(a + b),
            _ => panic!("unexpected types"),
        };
        // 4️⃣ Resume with the result
        let next = state.run(sum, &mut out)?;
        if let RunProgress::Complete(val) = next {
            println!("final value = {:?}", val); // → Int(7)
        }
    }
    Ok(())
}

```

### Python Bindings Example

The Python API mirrors the Rust flow, enabling async-friendly execution:

```python
from pydantic_monty import Monty, MontyRun

# 1️⃣ Define a script that awaits a coroutine provided by the host

code = """
async def main():
    x = await host_fetch("https://example.com")
    return len(x)
"""

# 2️⃣ Create the runner (external function is a coroutine)

runner = MontyRun(code, "script.py", [], ["host_fetch"])

# 3️⃣ Kick off execution – it will pause at the external call

progress = runner.start([], print_callback=print)

# 4️⃣ The progress is a FunctionCall; we tell the runtime we want async handling

if progress.is_function_call():
    # tell Monty to push a future instead of a concrete value

    progress = progress.run_pending()

# 5️⃣ The VM is now awaiting the future → ResolveFutures

if progress.is_resolve_futures():
    # Resolve the single pending fetch call with a mock result

    result = "hello world".encode()      # bytes returned by host_fetch

    progress = progress.resume([(0, result)])

# 6️⃣ Final result

assert progress.is_complete()
print("Length:", progress.value)  # → 11

```

## Key Source Files and Architecture

Understanding Monty's iterative execution requires familiarity with these core files:

- **[`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs)**: Contains `MontyRun`, `RunProgress`, `Snapshot`, and `FutureSnapshot`. This file implements the entire iterative execution engine, including `start()`, `run()`, and `resume()` methods.

- **[`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs)**: Implements `VM::run_module`, `VM::resume`, and `VM::add_pending_call`. This module contains the snapshot and restore logic that [`run.rs`](https://github.com/pydantic/monty/blob/main/run.rs) relies on to pause and resume execution.

- **[`crates/monty/src/bytecode/vm/async_exec.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/async_exec.rs)**: Handles future registration, resolution, and the `ResolveFutures` path. This file manages the async task scheduler that enables incremental resolution of pending futures.

- **[`crates/monty-python/src/monty_cls.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/monty_cls.rs)**: Exposes the iterative API (`start`, `run`, `run_pending`, `resume`) to Python users, wrapping the Rust `Snapshot` and `FutureSnapshot` types.

- **[`crates/monty-js/src/lib.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-js/src/lib.rs)**: Mirrors the Rust API for JavaScript/Node.js environments, handling serialization of snapshots across process boundaries.

## Summary

Monty's iterative execution architecture provides a robust foundation for sandboxed, asynchronous Python execution:

- **Incremental Execution**: The interpreter runs until it hits an external call or async future, then returns control to the host via the `RunProgress` enum.

- **State Snapshots**: Both `Snapshot` and `FutureSnapshot` own the entire interpreter state (heap, namespaces, VM), ensuring safe pause/resume semantics without host corruption.

- **Flexible Resumption**: Synchronous calls use `Snapshot::run` to inject results, while async operations use `FutureSnapshot::resume` to resolve multiple pending futures incrementally.

- **Cross-Platform Bindings**: The same iterative flow is exposed through Rust, Python (`pydantic_monty`), and JavaScript APIs, enabling consistent sandboxed execution across environments.

## Frequently Asked Questions

### What is the difference between `Snapshot` and `FutureSnapshot` in Monty?

`Snapshot` handles synchronous external function calls and OS operations, allowing the host to resume execution with a single return value or exception via `Snapshot::run`. `FutureSnapshot` manages asynchronous execution when the VM is blocked on multiple pending futures, enabling the host to resolve specific futures incrementally using `FutureSnapshot::resume` with a vector of `(call_id, result)` pairs.

### How does Monty handle exceptions during iterative execution?

When resuming execution, the host can pass either a return value or an exception to the snapshot. The `ExternalResult` enum supports both `Return(obj)` and `Error(exc)` variants. If an error is provided, `Snapshot::run` calls `VM::resume_with_exception`, which injects the exception into the Python stack frame as if it were raised natively, allowing standard Python exception handling to proceed.

### Can Monty's iterative execution run multiple scripts concurrently?

While a single `MontyRun` instance represents one isolated execution context, the architecture supports concurrent execution by creating multiple `MontyRun` instances, each with independent heaps and namespaces. Each instance can be paused and resumed independently via its own `Snapshot` or `FutureSnapshot`. The host is responsible for scheduling between these instances, as Monty itself does not provide a thread scheduler but rather the primitives for cooperative multitasking.

### What happens if the host provides an invalid `call_id` when resuming futures?

`FutureSnapshot::resume` validates all provided `call_id` values against the pending futures registered in the VM's async scheduler. If an unknown or already-resolved `call_id` is provided, the function returns a `MontyException` error before modifying any VM state. This validation ensures that the host cannot corrupt the interpreter state or resume execution with inconsistent future resolution data.