# How to Integrate Monty with Python Using PyO3: A Complete Guide

> Integrate Monty with Python using PyO3 to leverage a sandboxed Rust interpreter. Explore this comprehensive guide for seamless integration and enhanced performance.

- Repository: [Pydantic/monty](https://github.com/pydantic/monty)
- Tags: how-to-guide
- Published: 2026-02-19

---

**You can integrate Monty with Python using PyO3 by installing the `pydantic-monty` package and instantiating the `Monty` class from the `pydantic_monty` module, which exposes a sandboxed Rust-based interpreter through PyO3 bindings.**

Monty is a sandboxed Python interpreter written in Rust that provides secure code execution through PyO3 Python bindings. When you integrate Monty with Python using PyO3, you gain access to a restricted execution environment defined in the `pydantic/monty` repository, allowing you to run untrusted Python code safely while maintaining seamless data exchange between Rust and Python.

## Install the PyO3 Bindings for Monty

The `pydantic-monty` package bundles the compiled Rust library and the generated `pydantic_monty` module. Install it via pip:

```bash
pip install pydantic-monty

```

This command installs the wheel containing the PyO3 extension module, making the `Monty` class and exception types available in your Python environment.

## Basic Integration: Running Python Snippets

The `Monty` class defined in [`crates/monty-python/src/monty_cls.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/monty_cls.rs) parses Python source once during instantiation, then executes it multiple times with different inputs. This design minimizes overhead when running sandboxed code repeatedly.

```python
from pydantic_monty import Monty

code = """
def add(x, y):
    return x + y

result = add(a, b)
"""

# Declare which variables the code expects.

monty = Monty(code, inputs=["a", "b"])

# Execute with concrete values.

out = monty.run(inputs={"a": 10, "b": 32})
print(out)  # → 42

```

The `run` method performs the sandboxed execution using the pre-parsed bytecode from `MontyRun::new` in [`monty_cls.rs`](https://github.com/pydantic/monty/blob/main/monty_cls.rs), ensuring consistent behavior across multiple invocations.

## Register External Functions with PyO3

Monty supports calling back into host Python code via the **ExternalFunctionRegistry** implemented in [`crates/monty-python/src/external.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs). Supply a dictionary mapping external names to Python callables when invoking `run`.

```python
from pydantic_monty import Monty

def fetch(url: str) -> str:
    return f"fetched:{url}"

code = """
def wrapper(u):
    return fetch(u).upper()

result = wrapper('https://example.com')
"""

monty = Monty(
    code,
    inputs=[],
    external_functions=["fetch"]
)

out = monty.run(
    inputs={},
    external_functions={"fetch": fetch}
)
print(out)  # → "FETCHED:HTTPS://EXAMPLE.COM"

```

Under the hood, `run` builds an `ExternalFunctionRegistry` that looks up the name, converts Monty arguments to Python objects via `monty_to_py` in [`convert.rs`](https://github.com/pydantic/monty/blob/main/convert.rs), calls the callable, and converts the result back using `py_to_monty`.

## Enforce Resource Limits in the Sandboxed Environment

Restrict memory consumption, execution steps, or runtime by passing a **limits** dictionary to `run`. The limits are transformed into a `LimitedTracker` in the Rust core before execution begins.

```python
limits = {
    "max_steps": 1_000,      # maximum bytecode steps

    "max_memory": 5_000_000  # bytes

}
out = monty.run(inputs={"a": 5}, limits=limits)

```

These constraints prevent runaway code from consuming host resources, making Monty suitable for executing untrusted Python in production environments.

## Preserve Python Types with Dataclass Registration

When your sandboxed code returns `@dataclass` instances, you can preserve the original Python type by registering it with **dataclass_registry**. The registry is stored in the `PyMonty` instance and consulted during conversion in [`crates/monty-python/src/convert.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/convert.rs).

```python
from dataclasses import dataclass
from pydantic_monty import Monty

@dataclass
class Point:
    x: int
    y: int

code = """
def make():
    return Point(1, 2)
"""

monty = Monty(code, inputs=[], dataclass_registry=[Point])
result = monty.run()
print(isinstance(result, Point))  # → True

```

Without registration, the returned object would be a generic mapping rather than an instance of your specific dataclass.

## Handle Long-Running Code with Iterative Execution

For code that must pause for external I/O or async operations, use **start** and **MontySnapshot** instead of `run`. The `start` method initiates iterative execution and returns a `MontySnapshot` object that releases the GIL, allowing the host to perform blocking operations before resuming.

```python
from pydantic_monty import Monty, MontySnapshot

code = """
def long():
    data = fetch('url')
    return len(data)
"""

monty = Monty(code, external_functions=["fetch"])

snap = monty.start()
if isinstance(snap, MontySnapshot):
    # Perform blocking I/O here.

    snap = snap.resume(return_value="abcde")
print(snap)  # → 5

```

This pattern is essential for integrating Monty with Python using PyO3 in async applications or workflows requiring human-in-the-loop approval.

## Error Handling for PyO3 Integration

All Monty-raised errors map to specific Python exception classes defined in [`crates/monty-python/src/exceptions.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/exceptions.rs) and re-exported in [`lib.rs`](https://github.com/pydantic/monty/blob/main/lib.rs). These exceptions mirror CPython's format, making them seamless to catch.

| Monty exception | Python name |
|-----------------|-------------|
| `MontyError` | `pydantic_monty.MontyError` |
| `MontyRuntimeError` | `pydantic_monty.MontyRuntimeError` |
| `MontySyntaxError` | `pydantic_monty.MontySyntaxError` |
| `MontyTypingError` | `pydantic_monty.MontyTypingError` |

```python
import pydantic_monty

try:
    monty.run(code="1/0")
except pydantic_monty.MontyRuntimeError as exc:
    print(exc)  # ZeroDivisionError: division by zero

```

Catching these specific exceptions allows you to distinguish between syntax errors, runtime failures, and type violations when you integrate Monty with Python using PyO3.

## Complete Integration Example

The following script demonstrates the full PyO3 integration workflow: parsing code once, registering a dataclass, supplying an external callback, and handling errors.

```python
from dataclasses import dataclass
from pydantic_monty import Monty, MontyError

@dataclass
class Person:
    name: str
    age: int

def greet(person: Person) -> str:
    return f"Hello, {person.name}!"

code = """
def make():
    p = Person(name="Alice", age=30)
    return greet(p)
"""

monty = Monty(
    code,
    inputs=[],
    external_functions=["greet"],
    dataclass_registry=[Person],
)

try:
    result = monty.run(
        inputs={},
        external_functions={"greet": greet}
    )
    print(result)                  # → "Hello, Alice!"

    print(isinstance(result, str)) # True

except MontyError as e:
    print("Monty failed:", e)

```

This example leverages the `PyMonty` implementation in [`crates/monty-python/src/monty_cls.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/monty_cls.rs), the `ExternalFunctionRegistry` in [`external.rs`](https://github.com/pydantic/monty/blob/main/external.rs), and the bidirectional conversion logic in [`convert.rs`](https://github.com/pydantic/monty/blob/main/convert.rs) to create a seamless bridge between Rust and Python.

## Summary

- **Install** the PyO3 bindings via `pip install pydantic-monty` to access the `pydantic_monty` module.
- **Parse once, run many** by instantiating `Monty` with your code, then calling `run` with different inputs for efficient sandboxed execution.
- **Extend functionality** by registering external functions in the `ExternalFunctionRegistry` to allow sandboxed code to call host Python functions.
- **Preserve types** by registering dataclasses with `dataclass_registry` so returned objects maintain their original Python class identity.
- **Handle async workflows** using `start` and `MontySnapshot` for iterative execution that releases the GIL between steps.
- **Catch specific errors** using `MontyError`, `MontyRuntimeError`, and other exception classes defined in [`exceptions.rs`](https://github.com/pydantic/monty/blob/main/exceptions.rs) to handle sandbox failures gracefully.

## Frequently Asked Questions

### How do I install the Monty PyO3 bindings for Python?

Install the pre-compiled wheel from PyPI using `pip install pydantic-monty`. This package bundles the Rust library built with PyO3 and exposes the `pydantic_monty` module containing the `Monty` class and exception types.

### Can I pass custom Python functions into the Monty sandbox?

Yes. Declare the function names in the `external_functions` parameter when constructing `Monty`, then provide the actual callables in the `external_functions` dictionary passed to `run`. The `ExternalFunctionRegistry` in [`crates/monty-python/src/external.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs) handles the conversion between Monty objects and Python objects via the `monty_to_py` and `py_to_monty` functions in [`convert.rs`](https://github.com/pydantic/monty/blob/main/convert.rs).

### How does Monty handle Python dataclasses when returning objects to the host?

When you register a dataclass using the `dataclass_registry` parameter, Monty stores the type in the `PyMonty` instance. During object conversion in [`crates/monty-python/src/convert.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/convert.rs), the `monty_to_py` function checks this registry and reconstructs the original Python dataclass instance, preserving `isinstance` relationships and attribute access.

### What is the difference between `run` and `start` in the Monty PyO3 API?

The `run` method executes code synchronously to completion, returning the final result or raising a `MontyError` exception. The `start` method initiates iterative execution and returns a `MontySnapshot` object that releases the GIL, allowing the host to perform blocking I/O or async operations. You resume execution by calling `resume` on the snapshot with the required return values, making `start` ideal for integrating Monty with Python using PyO3 in async applications or workflows requiring human-in-the-loop approval.