# How Monty's Serialization Works for Execution State

> Discover how Monty serializes execution state. Learn how VM components are captured in VMSnapshot and encoded into a compact binary blob using postcard for later restoration.

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

---

**Monty serializes execution state by moving ownership of the VM's operand stack, call frames, and heap into a `VMSnapshot` struct, wrapping it with the executor and namespaces in a serde-enabled `Snapshot<T>`, and encoding the result via `postcard` into a compact binary blob that can be restored later.**

Monty, the sandboxed Python interpreter from `pydantic/monty`, enables programs to pause at external function calls or async await points and resume later—even across different processes—through sophisticated execution state serialization. This system captures the entire virtual machine state without expensive cloning operations, allowing deterministic pause and resume semantics.

## Core Components: VMSnapshot and Snapshot Types

Monty's serialization architecture separates the raw VM state from the higher-level container that includes the heap and compiled bytecode. This separation enables zero-copy capture of the execution stack while still providing a fully serializable package.

### VMSnapshot: The Raw VM State

The `VMSnapshot` struct holds the transient execution data that changes as Python code runs. According to the source in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) (lines 63-78), it stores:

- The operand stack (`Vec<Value>`)
- The call frame list (`Vec<CallFrame>`)
- The exception stack
- The current instruction pointer (IP)
- The next external call ID counter
- An optional async scheduler for pending futures

When the VM pauses, `VM::snapshot()` consumes the live VM and moves these vectors directly into the `VMSnapshot` without cloning. This ownership transfer is critical for performance: reference counts on heap objects remain unchanged because the VM already owned them.

### Snapshot<T> and FutureSnapshot<T>: Serializable Containers

While `VMSnapshot` captures the execution pointer, it does not include the heap or the compiled bytecode. The `Snapshot<T>` and `FutureSnapshot<T>` structs defined in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) (lines 14-27) serve as the top-level containers that bundle:

- The **executor** (compiled bytecode and intern tables)
- The **heap** (all Python objects referenced by the stack)
- The **namespaces** (global and local variable bindings)
- The **VMSnapshot** (execution state)
- A generic resource tracker `T` for custom limits (time, memory, etc.)

Both structs derive `serde::Serialize` and `serde::Deserialize` with bounds requiring `T: Serialize + DeserializeOwned`, enabling them to be encoded to any serde-compatible format.

## The Serialization Process: From Live VM to Binary Blob

Monty converts a running Python program into a storable binary format through a deterministic sequence of ownership transfers and encoding steps.

### Detecting Pause Points with check_snapshot()

The VM does not snapshot on every instruction—that would be prohibitively expensive. Instead, `VM::check_snapshot()` in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) (lines 47-55) determines whether the current `FrameExit` result warrants a pause. Snapshots are created only when the VM returns:

- `ExternalCall` (calling a host function)
- `OsCall` (operating system interaction)
- `ResolveFutures` (awaiting async completion)

This selective approach ensures that pure Python code runs at full speed, while boundaries between the sandbox and the host are capture points.

### Zero-Copy State Capture with snapshot()

When a pause is triggered, `VM::snapshot()` consumes the VM instance (taking `self` by value) and constructs a `VMSnapshot`. As implemented in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) (lines 61-68), this method moves the operand stack, call frames, and exception stack vectors directly into the snapshot struct. Because these are moved rather than cloned, and because `Value` objects on the stack are reference-counted handles to the heap, no reference count adjustments are necessary. The snapshot now owns the exact same state the VM had at the pause point.

### Serde Serialization and postcard Encoding

With the `VMSnapshot` prepared, Monty wraps it in a `Snapshot<T>` or `FutureSnapshot<T>` along with the executor, heap, and namespaces. The `dump()` method in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) (lines 99-108) serializes this container using `postcard::to_allocvec`, producing a compact, self-contained binary blob. The `load()` method (lines 110-119) reverses this process using `postcard::from_bytes` to reconstruct the `Snapshot<T>`.

## Restoring Execution State

Deserialization reconstructs not just the data, but the exact execution context required to continue running Python code.

### Reconstructing the VM with restore()

The `Snapshot::run()` or `FutureSnapshot::resume()` methods invoke `VM::restore()` with the stored `VMSnapshot`. As defined in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) (lines 90-106), this method:

1. Reconstructs each `CallFrame` from the serialized representation, relinking function IDs to the `Interns` table in the executor
2. Reinstates the operand stack, exception stack, and instruction pointer
3. Restores the external call ID counter and async scheduler state

The result is a fully functional VM positioned at the exact bytecode instruction where it paused, with all heap objects and namespace bindings intact.

## Code Examples

### Caching a Compiled Program for Later Reuse

This example demonstrates serializing the `MontyRun` container itself, which includes the compiled bytecode and intern tables, allowing you to skip parsing on subsequent runs.

```rust
use monty::{MontyRun, MontyObject};

let runner = MontyRun::new(
    "def inc(x): return x+1\ninc(a)".to_owned(),
    "example.py",
    vec!["a".to_owned()],
    vec![],               // no external functions
).unwrap();

// First execution (no snapshotting)
let result = runner.run_no_limits(vec![MontyObject::Int(41)]).unwrap();
assert_eq!(result, MontyObject::Int(42));

// Serialize the *runner* (parsed code, intern tables) for caching
let serialized = runner.dump().unwrap();
// Store `serialized` somewhere (file, DB, …)

// Later… reconstruct the runner without reparsing
let cached_runner = MontyRun::load(&serialized).unwrap();
let result2 = cached_runner.run_no_limits(vec![MontyObject::Int(7)]).unwrap();
assert_eq!(result2, MontyObject::Int(8));

```

*Key implementation details*: `MontyRun::dump()` is defined in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) at lines 99-108, while `MontyRun::load()` resides at lines 110-119.

### Pausing at External Function Calls

When the VM encounters a host function, it returns control to the caller along with a snapshot that can be resumed later.

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

// A function that will be supplied by the host
let external_funcs = vec!["host_print".to_string()];

let runner = MontyRun::new(
    "def foo(): host_print('hello')\nfoo()".to_owned(),
    "host_demo.py",
    vec![],
    external_funcs,
).unwrap();

// Start execution – it stops at the external call
let progress = runner.start(vec![]).unwrap(); // Returns RunProgress::FunctionCall
match progress {
    RunProgress::FunctionCall(call) => {
        // Host supplies the result (here we just return None)
        let snapshot = call.snapshot; // This is a Snapshot<T>
        let resumed = snapshot.run(
            ExternalResult::Return(MontyObject::None), 
            &mut PrintWriter::Stdout
        ).unwrap();
        // `resumed` is either Complete, another FunctionCall, or ResolveFutures
        println!("Finished: {:?}", resumed);
    }
    _ => unreachable!(),
}

```

*Key implementation details*: `Snapshot::run()` is implemented in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) at lines 72-84, while `VM::restore()` is defined in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) at lines 90-106.

### Incrementally Resolving Async Futures

For asynchronous code, `FutureSnapshot` captures the scheduler state, allowing incremental resolution of pending futures.

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

let runner = MontyRun::new(
    "import asyncio\nasync def work(): await asyncio.sleep(1)\nasyncio.run(work())".to_owned(),
    "async_demo.py",
    vec![],
    vec![],
).unwrap();

let mut progress = runner.start(vec![]).unwrap(); // Pause at first future
while let RunProgress::ResolveFutures(state) = progress {
    // Resolve a subset of pending futures (here we just mark all as done)
    let results = state.pending_call_ids()
        .iter()
        .map(|&id| (id, MontyObject::None))
        .collect();
    progress = state.resume(results, &mut PrintWriter::Stdout).unwrap();
}
assert!(matches!(progress, RunProgress::Complete(_)));

```

*Key implementation details*: `FutureSnapshot::run()` is defined in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) at lines 50-57, with `FutureSnapshot::resume()` at lines 81-89.

## Summary

- **Zero-copy snapshots**: Monty moves ownership of the VM's operand stack, call frames, and exception stack directly into `VMSnapshot` without cloning, preserving reference counts.
- **Serde-enabled containers**: `Snapshot<T>` and `FutureSnapshot<T>` bundle the VM state with the heap, namespaces, and executor, implementing `Serialize` and `Deserialize` for cross-process portability.
- **Selective pausing**: The VM only snapshots when encountering external calls or async boundaries, detected by `VM::check_snapshot()` in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs).
- **Deterministic restoration**: `VM::restore()` reconstructs call frames by relinking function IDs to intern tables, allowing execution to continue from the exact instruction pointer where it paused.

## Frequently Asked Questions

### What is the difference between VMSnapshot and Snapshot<T>?

`VMSnapshot` is an internal struct defined in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) that holds only the transient execution data—operand stack, call frames, instruction pointer, and exception state. `Snapshot<T>` is the public-facing container in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) that combines a `VMSnapshot` with the heap, namespaces, executor, and a generic resource tracker `T`, implementing serde traits for full serialization.

### How does Monty achieve zero-copy serialization?

When `VM::snapshot()` is called, it consumes the VM instance by taking `self` and moves its internal vectors (operand stack, frames, exception stack) directly into the `VMSnapshot` struct without cloning. Because `Value` objects on the stack are reference-counted handles, this transfer preserves all reference counts and avoids expensive deep copies of heap objects, as implemented in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) lines 61-68.

### Can Monty serialize asynchronous execution state?

Yes, Monty supports serializing async state through `FutureSnapshot<T>`, which includes an optional `Scheduler` field inside `VMSnapshot` to track pending tasks. When the VM pauses at a `ResolveFutures` boundary, `FutureSnapshot::resume()` allows incremental resolution of futures, enabling the program to continue async execution after deserialization even if resumed in a different process.

### What serialization format does Monty use?

Monty uses the `postcard` crate to encode `Snapshot<T>` and `FutureSnapshot<T>` into a compact, binary format. The `dump()` method in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) (lines 99-108) calls `postcard::to_allocvec`, while `load()` (lines 110-119) uses `postcard::from_bytes` for reconstruction, providing a space-efficient representation suitable for caching and network transmission.