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

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. This method consumes a MontyRun configuration, initializes the heap and namespaces, creates a VM instance, and begins execution:

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

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 represents the possible states when execution pauses:

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

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:

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 and run.rs:121-124

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:

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 and detailed logic at run.rs:124-154

Practical Implementation Examples

Rust Implementation

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

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:

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: 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: Implements VM::run_module, VM::resume, and VM::add_pending_call. This module contains the snapshot and restore logic that run.rs relies on to pause and resume execution.

  • 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: 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: 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →