Torna agli aggiornamenti
New releaseJul 25, 2026

monty v0.0.19

Un interprete Python minimale e sicuro scritto in Rust per uso da parte dell'IA

Condividi

Monty

Un interprete Python minimale e sicuro scritto in Rust, pensato per l'uso da parte di IA.

CI Codspeed Coverage PyPI versions license Join Slack

Sperimentale - Questo progetto è ancora in fase di sviluppo e non è pronto per l'uso in produzione.

Un interprete Python minimale e sicuro scritto in Rust, pensato per l'uso da parte di IA.

Monty evita i costi, la latenza, la complessità e il fastidio generale di usare una sandbox completa basata su container per eseguire codice generato da LLM.

Invece, ti permette di eseguire in sicurezza codice Python scritto da un LLM integrato nel tuo agente, con tempi di avvio misurati in microsecondi a una cifra, non in centinaia di millisecondi.

Cosa Monty può fare:

  • Eseguire un sottoinsieme ragionevole di codice Python, sufficiente affinché il tuo agente possa esprimere ciò che vuole fare
  • Bloccare completamente l'accesso all'ambiente host: filesystem, variabili d'ambiente e accesso alla rete sono tutti implementati tramite chiamate a funzioni esterne che lo sviluppatore può controllare
  • Chiamare funzioni sull'host, solo quelle a cui gli concedi l'accesso
  • Eseguire il typechecking: Monty supporta i moderni type hint di Python e include ty in un singolo binario per eseguire il controllo dei tipi
  • Essere serializzato in byte in corrispondenza delle chiamate a funzioni esterne, consentendo di salvare lo stato dell'interprete in un file o in un database e riprenderlo in seguito
  • Avviarsi estremamente in fretta (<1μs dal codice al risultato dell'esecuzione) e avere prestazioni a runtime simili a CPython (in genere tra 5 volte più veloce e 5 volte più lento)
  • Essere richiamato da Rust, Python o JavaScript: poiché Monty non ha dipendenze da CPython, puoi usarlo ovunque riesca a eseguire Rust
  • Controllare l'uso delle risorse: Monty può tracciare l'uso della memoria, la profondità dello stack e il tempo di esecuzione e annullare l'esecuzione se supera i limiti predefiniti
  • Raccogliere stdout e stderr e restituirli al chiamante
  • Eseguire codice async o sync sull'host tramite codice async o sync sull'host
  • Usare un piccolo sottoinsieme della libreria standard: sys, os, typing, asyncio, re, datetime, json, dataclasses (presto)

Cosa Monty non può fare:

  • Usare il resto della libreria standard
  • Usare librerie di terze parti (come Pydantic); il supporto per librerie Python esterne non è un obiettivo
  • definire classi (il supporto arriverà presto)
  • usare i match statement (anche qui, il supporto arriverà presto)

In breve, Monty è estremamente limitato e progettato per un caso d'uso:

Eseguire codice scritto da agenti.

Per capire perché potresti volerlo fare, vedi:

In parole molto semplici, l'idea alla base di tutto ciò è che gli LLM possono lavorare più velocemente, a costi inferiori e in modo più affidabile se viene chiesto loro di scrivere codice Python (o JavaScript) invece di affidarsi alle tradizionali chiamate a strumenti. Monty rende tutto questo possibile senza la complessità di una sandbox o il rischio di eseguire codice direttamente sull'host.

Nota: Monty verrà (presto) utilizzato per implementare codemode in Pydantic AI

Utilizzo

Monty può essere richiamato da Python, JavaScript/TypeScript o Rust.

Python

Per installare:```bash uv add pydantic-monty

(Oppure `pip install pydantic-monty` per i boomer)

`pydantic-monty` è un metapackage che abbina `pydantic-monty-client` (il
modulo `pydantic_monty`) con `pydantic-monty-runtime` (il binario del
worker `monty`). Installa `pydantic-monty-client` da solo se il binario proviene già da
un'altra parte.

Uso:```python
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())

L'esecuzione avviene in un pool di subprocessi worker monty, quindi anche un errore di memoria innescato da codice avversario (stack overflow, abort dell'allocatore) non può mai mandare in crash il tuo processo — il worker muore, solleva MontyCrashedError e viene sostituito. C'è anche un'API completamente sincrona:```python 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

### JavaScript / TypeScript

Per installare:```bash
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:```ts 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' }, })

Per i browser (o ovunque i sottoprocessi siano impossibili) lo stesso pacchetto
espone una build WebAssembly in-process tramite il sottopercorso `@pydantic/monty/wasm`
(nessun isolamento dai crash: un crash della sandbox lì è un crash dell'host).

### Rust

Per eseguire codice non fidato da Rust, consigliamo la
crate [`monty-pool`](https://crates.io/crates/monty-pool) piuttosto che l'API in-process qui sotto.
`monty-pool` esegue codice solo in sottoprocessi worker `monty`, il che offre protezioni aggiuntive:
un crash causato da codice avversario (overflow dello stack, abort dell'allocatore) uccide solo il worker —
il pool rileva la morte e sostituisce il worker — e un watchdog lato genitore può uccidere i worker
che superano un timeout rigido. È lo stesso motore su cui sono costruiti i pacchetti Python e JavaScript sopra.
Vedi il [README di monty-pool](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
per l'utilizzo.

La crate `monty` stessa fornisce l'interprete in-process:```rust
use monty::MontyRun;
use monty_types::{CompileOptions, ResourceTracker, MontyObject, PrintWriter, ResourceLimits};

let code = r#"
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

fib(x)
"#;

let runner = MontyRun::new(code.to_owned(), "fib.py", vec!["x".to_owned()], CompileOptions::default()).unwrap();
let result = runner.run(vec![MontyObject::Int(10)], ResourceTracker::default(), PrintWriter::Stdout).unwrap();
assert_eq!(result, MontyObject::Int(55));

Serializzazione

Una sessione REPL può essere serializzata con dump() e ripristinata con Dump::load(). Il dump trasporta i metadati della sessione (nome dello script, stub di type-check) insieme allo stato dell'interprete, con una versione che la build di caricamento controlla:```rust use monty::{Dump, MontyRepl, Session, SessionRef, dump}; use monty_types::{CompileOptions, MontyObject, PrintWriter, ResourceTracker};

// Snapshot a session between snippets let mut repl = MontyRepl::new("main.py", ResourceTracker::default(), CompileOptions::default()); repl.feed_run("x = 41", vec![], PrintWriter::Stdout).unwrap(); let bytes = dump("main.py", None, SessionRef::Idle(&repl)).unwrap();

// Later, restore and carry on feeding let Session::Idle(mut restored) = Dump::load(&bytes).unwrap().state else { panic!("dumped an idle session") }; let result = restored.feed_run("x + 1", vec![], PrintWriter::Stdout).unwrap(); assert_eq!(result, MontyObject::Int(42));

`MontyRun` e `RunProgress` non hanno un formato di dump proprio, ma entrambi implementano `serde::Serialize`/`Deserialize`, quindi un host può serializzare il codice parsato o un'esecuzione in pausa con qualsiasi formato già in uso.

## Limiti di memoria nei worker

Il `max_memory` di una sessione è misurato dall'allocator del worker. L'interprete
segnala un `MemoryError` controllato dopo aver superato il limite soft; un limite hard
più elevato termina e sostituisce il worker se un'allocazione compie un salto eccessivo tra i checkpoint.

Vedi [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) per come
il superamento di un limite si manifesta verso un host e `monty-alloc` per l'allocator
sotto cui girano sia i worker di sottoprocessi che quelli WebAssembly.

## Integrazione con PydanticAI

Monty alimenterà la modalità codice in
[Pydantic AI](https://github.com/pydantic/pydantic-ai). Invece di effettuare
chiamate di strumenti sequenziali, l'LLM scrive codice Python che chiama i tuoi strumenti
come funzioni e Monty lo esegue in modo sicuro.```python test="skip"
import asyncio
import json

import logfire
from httpx import AsyncClient
from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.code_mode import CodeModeToolset
from pydantic_ai.toolsets.function import FunctionToolset
from typing_extensions import TypedDict

logfire.configure()
logfire.instrument_pydantic_ai()


class LatLng(TypedDict):
    lat: float
    lng: float


weather_toolset: FunctionToolset[AsyncClient] = FunctionToolset()


@weather_toolset.tool
async def get_lat_lng(
    ctx: RunContext[AsyncClient], location_description: str
) -> LatLng:
    """Get the latitude and longitude of a location."""
    # NOTE: the response here will be random, and is not related to the location description.
    r = await ctx.deps.get(
        'https://demo-endpoints.pydantic.workers.dev/latlng',
        params={'location': location_description},
    )
    r.raise_for_status()
    return json.loads(r.content)


@weather_toolset.tool
async def get_temp(ctx: RunContext[AsyncClient], lat: float, lng: float) -> float:
    """Get the temp at a location."""
    # NOTE: the responses here will be random, and are not related to the lat and lng.
    r = await ctx.deps.get(
        'https://demo-endpoints.pydantic.workers.dev/number',
        params={'min': 10, 'max': 30},
    )
    r.raise_for_status()
    return float(r.text)


@weather_toolset.tool
async def get_weather_description(
    ctx: RunContext[AsyncClient], lat: float, lng: float
) -> str:
    """Get the weather description at a location."""
    # NOTE: the responses here will be random, and are not related to the lat and lng.
    r = await ctx.deps.get(
        'https://demo-endpoints.pydantic.workers.dev/weather',
        params={'lat': lat, 'lng': lng},
    )
    r.raise_for_status()
    return r.text


agent = Agent(
    'gateway/anthropic:claude-sonnet-4-5',
    # toolsets=[weather_toolset],
    toolsets=[CodeModeToolset(weather_toolset)],
    deps_type=AsyncClient,
)


async def main():
    async with AsyncClient() as client:
        await agent.run('Compare the weather of London, Paris, and Tokyo.', deps=client)


if __name__ == '__main__':
    asyncio.run(main())

Community Bindings

  • Go: gomonty - Binding Go per l'interprete Monty
  • Dart/Flutter: dart_monty (github) (pub.dev)- Binding Dart/Flutter per Monty

Alternative

In genere ci sono due reazioni quando mostri Monty alle persone:

  1. Oh mio dio, risolve così tanti problemi, lo voglio.
  2. Perché non X?

Dove X è una qualche tecnologia alternativa. Stranamente queste reazioni sono spesso combinate, suggerendo che le persone non hanno ancora trovato un'alternativa che funzioni per loro, ma sono increduli che non esista davvero una buona alternativa al dover creare da zero un'intera implementazione di Python.

NOTA: tutte queste tecnologie sono impressionanti e hanno usi diffusi; questo commento sulle loro limitazioni per il nostro caso d'uso non deve essere visto come una critica. La maggior parte di queste soluzioni non è stata concepita con l'obiettivo di fornire una sandbox per LLM, motivo per cui non sono necessariamente ottime in questo.

TecnologiaCompletezza del linguaggioSicurezzaLatenza di avvioFOSSComplessità di configurazioneMontaggio fileSnapshot
Montyparzialerigorosa0.06msgratuito / OSSfacilefacilefacile
Dockercompletabuona195msgratuito / OSSintermediafacileintermedia
Pyodidecompletascarsa2800msgratuito / OSSintermediafaciledifficile
starlark-rustmolto limitatabuona1.7msgratuito / OSSfacilenon disponibile?impossibile?
WASI / Wasmerparziale, quasi completarigorosa66msgratuito *intermediafacileintermedia
servizio di sandboxingcompletarigorosa1033msa pagamentointermediadifficileintermedia
YOLO Pythoncompletainesistente0.1ms / 30msgratuito / OSSfacilefacile / spaventosodifficile

Vedi ./scripts/startup_performance.py per lo script usato per calcolare i valori delle prestazioni di avvio.

Dettagli su ogni riga qui di seguito:

Monty

  • Completezza del linguaggio: Nessuna classe (per ora), stdlib limitata, nessuna libreria di terze parti
  • Sicurezza: Accesso esplicitamente controllato a filesystem, rete e variabili d'ambiente, limiti rigorosi su tempo di esecuzione e uso della memoria
  • Latenza di avvio: Si avvia in microsecondi
  • Complessità di configurazione: basta pip install pydantic-monty o npm install @pydantic/monty, download di ~4.5MB
  • Montaggio file: Strettamente controllato, vedi #85
  • Snapshot: La funzionalità di pausa e ripresa di Monty con dump() e load() rende banale mettere in pausa, riprendere e fare il fork dell'esecuzione

Docker

  • Completezza del linguaggio: CPython completo con qualsiasi libreria
  • Sicurezza: Isolamento di processi e filesystem, policy di rete, ma esistono tecniche di fuga dal container, è possibile limitare la memoria
  • Latenza di avvio: Overhead di avvio del container (~195ms misurati)
  • Complessità di configurazione: Richiede il daemon Docker, immagini container, orchestrazione, python:3.14-alpine è 50MB - docker non può essere installato da PyPI
  • Montaggio file: I mount di volumi funzionano bene
  • Snapshot: Possibile con soluzioni di esecuzione durevole come Temporal, oppure creando uno snapshot di un'immagine e salvandola come immagine Docker.

Pyodide

  • Completezza del linguaggio: CPython completo compilato in WASM, quasi tutte le librerie disponibili
  • Sicurezza: Si affida alla sandbox del browser/WASM - non progettata per l'isolamento lato server, il codice Python può eseguire codice arbitrario nel runtime JS, solo deno consente l'isolamento, i limiti di memoria sono difficili/impossibili da imporre con deno
  • Latenza di avvio: Il caricamento del runtime WASM è lento (~2800ms a freddo)
  • Complessità di configurazione: È necessario caricare il runtime WASM, gestire l'inizializzazione asincrona, il pacchetto NPM di pyodide è ~12MB, deno è ~50MB - Pyodide non può essere chiamato solo con pacchetti PyPI
  • Montaggio file: Filesystem virtuale tramite API del browser
  • Snapshot: Possibile presumibilmente con soluzioni di esecuzione durevole come Temporal, ma difficile

starlark-rust

Vedi starlark-rust.

  • Completezza del linguaggio: Linguaggio di configurazione, non Python - niente classi, eccezioni, async
  • Sicurezza: deterministico ed ermetico per progettazione
  • Latenza di avvio: gira integrato nel processo come Monty, quindi un tempo di avvio impressionante
  • Complessità di configurazione: Utilizzabile in Python tramite starlark-pyo3
  • Montaggio file: Nessuna gestione dei file per progettazione, per quanto ne so?
  • Snapshot: Impossibile, per quanto ne so?

WASI / Wasmer

Esecuzione di Python in WebAssembly tramite Wasmer.

  • Completezza del linguaggio: CPython completo, i pacchetti esterni puramente Python funzionano tramite mount, i pacchetti esterni con binding C non funzionano
  • Sicurezza: In linea di principio WebAssembly dovrebbe fornire forti garanzie di sandboxing.
  • Latenza di avvio: Il pacchetto Python wasmer non viene aggiornato da 3 anni e non sono riuscito a trovare documentazione su come chiamare Python in wasmer da Python, quindi l'ho chiamato tramite subprocess. La latenza di avvio era 66ms.
  • Complessità di configurazione: Il download di wasmer è 100mb, il pacchetto "python/python" è 50mb.
  • FOSS: L'ho contrassegnato come "gratuito *" perché il costo è zero ma non tutto sembra essere open source. Al 2026-02-10 il pacchetto python/python di wasmer non ha readme, né licenza, né link alla sorgente e nessuna indicazione su come è stato compilato; le versioni caricate di recente mostrano una dimensione di "0B" anche se il download è di ~50MB - il processo di build del binario Python non è chiaro e trasparente. (Se mi sbaglio, per favore apri un'issue per correggermi)
  • Montaggio file: Supportato
  • Snapshot: Supportato tramite journaling

servizio di sandboxing

Servizi come Daytona, E2B, Modal.

Ci sono sfide simili: maggiore complessità di configurazione ma una latenza di rete inferiore, se si configura la propria sandbox con k8s.

  • Completezza del linguaggio: CPython completo con qualsiasi libreria
  • Sicurezza: Isolamento dei container gestito professionalmente
  • Latenza di avvio: Round-trip di rete e tempo di avvio del container. Ho ottenuto un tempo di avvio a freddo di ~1s con Daytona EU da Londra; Daytona pubblicizza una latenza inferiore a 90ms, presumibilmente per un container già esistente, non è chiaro se includa la latenza di rete.
  • FOSS: Si paga per esecuzione o tempo di calcolo, alcune implementazioni sono open source
  • Complessità di configurazione: Integrazione API, token di autenticazione - adatto alle startup ma generalmente non è un'opzione per le imprese
  • Montaggio file: Caricamento/scaricamento tramite chiamate API
  • Snapshot: Possibile con soluzioni di esecuzione durevole come Temporal; anche i servizi offrono alcune soluzioni per questo, credo basate su container Docker.

YOLO Python

Eseguire Python direttamente tramite exec() (~0.1ms) o subprocess (~30ms).

  • Completezza del linguaggio: CPython completo con qualsiasi libreria
  • Sicurezza: Nessuna - accesso completo a filesystem, rete, variabili d'ambiente, comandi di sistema
  • Latenza di avvio: Quasi zero per exec(), ~30ms per subprocess
  • Complessità di configurazione: Nessuna
  • Montaggio file: Accesso diretto al filesystem (è questo il problema)
  • Snapshot: Possibile con soluzioni di esecuzione durevole come Temporal

Parte del Pydantic Stack

Il Pydantic Stack è tutto ciò che serve per distribuire agenti AI di livello produzione:

Categorie