How to Integrate Monty with Python Using PyO3: A Complete Guide
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:
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 parses Python source once during instantiation, then executes it multiple times with different inputs. This design minimizes overhead when running sandboxed code repeatedly.
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, 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. Supply a dictionary mapping external names to Python callables when invoking run.
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, 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.
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.
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.
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 and re-exported in 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 |
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.
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, the ExternalFunctionRegistry in external.rs, and the bidirectional conversion logic in convert.rs to create a seamless bridge between Rust and Python.
Summary
- Install the PyO3 bindings via
pip install pydantic-montyto access thepydantic_montymodule. - Parse once, run many by instantiating
Montywith your code, then callingrunwith different inputs for efficient sandboxed execution. - Extend functionality by registering external functions in the
ExternalFunctionRegistryto allow sandboxed code to call host Python functions. - Preserve types by registering dataclasses with
dataclass_registryso returned objects maintain their original Python class identity. - Handle async workflows using
startandMontySnapshotfor iterative execution that releases the GIL between steps. - Catch specific errors using
MontyError,MontyRuntimeError, and other exception classes defined inexceptions.rsto 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 handles the conversion between Monty objects and Python objects via the monty_to_py and py_to_monty functions in 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, 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →