
A secure Python sandbox, written in Rust, for code written by AI.
Experimental - This project is still in development, and not ready for prime time.
A minimal, secure Python interpreter written in Rust for use by AI.
Monty avoids the cost, latency, complexity and general faff of using a full container based sandbox for running LLM generated code.
Instead, it lets you safely run Python code written by an LLM embedded in your agent, with startup times measured in single digit microseconds not hundreds of milliseconds.
What Monty can do:
sys, os, typing, asyncio, re, datetime, json, dataclasses (soon)What Monty cannot do:
In short, Monty is extremely limited and designed for one use case:
To run code written by agents.
For motivation on why you might want to do this, see:
In very simple terms, the idea of all the above is that LLMs can work faster, cheaper and more reliably if they're asked to write Python (or Javascript) code, instead of relying on traditional tool calling. Monty makes that possible without the complexity of a sandbox or risk of running code directly on the host.
Note: Monty will (soon) be used to implement codemode in Pydantic AI
Monty can be called from Python, JavaScript/TypeScript or Rust.
To install:
uv add pydantic-monty
(Or pip install pydantic-monty for the boomers)
pydantic-monty is a metapackage pairing pydantic-monty-client (the
pydantic_monty module) with pydantic-monty-runtime (the monty worker
binary). Install pydantic-monty-client alone if the binary already comes from
somewhere else.
Usage:
from typing import Any
import pydantic_monty
code = """
async def agent(prompt: str, messages: Messages):
while True:
print(f'messages so far: {messages}')
output = await call_llm(prompt, messages)
if isinstance(output, str):
return output
messages.extend(output)
await agent(prompt, [])
"""
type_definitions = """
from typing import Any
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
raise NotImplementedError()
prompt: str = ''
"""
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
if len(messages) < 2:
return [{'role': 'system', 'content': 'example response'}]
else:
return f'example output, message count {len(messages)}'
async def main():
async with pydantic_monty.AsyncMonty() as pool:
async with pool.checkout(
script_name='agent.py',
type_check=True,
type_check_stubs=type_definitions,
) as session:
output = await session.feed_run(
code,
inputs={'prompt': 'testing'},
external_lookup={'call_llm': call_llm},
)
print(output)
#> example output, message count 2
if __name__ == '__main__':
import asyncio
asyncio.run(main())
Execution happens in a pool of monty worker subprocesses, so even a memory
error triggered by adversarial code (stack overflow, allocator abort) can
never crash your process — the worker dies, raises MontyCrashedError, and
is replaced. There is also a fully synchronous API:
import pydantic_monty
with pydantic_monty.Monty() as pool:
with pool.checkout() as session:
# session state persists between feed_run calls
session.feed_run('x = 21')
print(session.feed_run('x * 2'))
#> 42
To install:
npm install @pydantic/monty
The JS package is a native (napi) binding over the same Rust worker pool the
Python package uses — the binding and the monty worker binary ship via
platform-specific npm packages:
import { Monty } from '@pydantic/monty'
await using pool = await Monty.create()
await using session = await pool.checkout()
// session state persists between feedRun calls
await session.feedRun('x = 21')
console.log(await session.feedRun('x * 2')) // 42
// external functions may be async
const result = await session.feedRun('await fetch_data()', {
externalLookup: { fetch_data: async () => 'data' },
})
For browsers (or anywhere subprocesses are impossible) the same package
exposes an in-process WebAssembly build under the @pydantic/monty/wasm
subpath (no crash isolation: a sandbox crash is a host crash there).