Retour aux mises à jour
New releaseJul 25, 2026

monty v0.0.19

Un interpréteur Python minimal et sécurisé écrit en Rust pour utilisation par l'IA.

Partager

Monty

Un interpréteur Python minimal et sécurisé écrit en Rust pour être utilisé par l'IA.

CI Codspeed Coverage PyPI versions license Join Slack

Expérimental - Ce projet est encore en développement et n'est pas prêt pour une utilisation en production.

Un interpréteur Python minimal et sécurisé écrit en Rust pour être utilisé par l'IA.

Monty évite le coût, la latence, la complexité et les tracas d'un sandbox complet basé sur des conteneurs pour exécuter du code généré par un LLM.

Au lieu de cela, il vous permet d'exécuter en toute sécurité du code Python écrit par un LLM intégré à votre agent, avec des temps de démarrage mesurés en microsecondes à un chiffre, et non en centaines de millisecondes.

Ce que Monty peut faire :

  • Exécuter un sous-ensemble raisonnable de code Python - suffisant pour que votre agent exprime ce qu'il veut faire
  • Bloquer complètement l'accès à l'environnement hôte : le système de fichiers, les variables d'environnement et l'accès réseau sont tous implémentés via des appels de fonctions externes que le développeur peut contrôler
  • Appeler des fonctions sur l'hôte - uniquement les fonctions auxquelles vous lui donnez accès
  • Exécuter la vérification de types - monty prend en charge les annotations de types Python modernes complètes et est fourni avec ty inclus dans un seul binaire pour exécuter la vérification de types
  • Être instantanément sérialisé en octets lors des appels de fonctions externes, ce qui signifie que vous pouvez stocker l'état de l'interpréteur dans un fichier ou une base de données, et reprendre plus tard
  • Démarrer extrêmement rapidement (<1μs entre le code et le résultat d'exécution), et avoir des performances d'exécution similaires à celles de CPython (généralement entre 5 fois plus rapides et 5 fois plus lentes)
  • Être appelé depuis Rust, Python ou Javascript - parce que Monty n'a aucune dépendance à CPython, vous pouvez l'utiliser partout où vous pouvez exécuter du Rust
  • Contrôler l'utilisation des ressources - Monty peut suivre l'utilisation de la mémoire, la profondeur de la pile et le temps d'exécution, et annuler l'exécution si elle dépasse les limites prédéfinies
  • Collecter stdout et stderr et les renvoyer à l'appelant
  • Exécuter du code asynchrone ou synchrone sur l'hôte via du code asynchrone ou synchrone sur l'hôte
  • Utiliser un petit sous-ensemble de la bibliothèque standard : sys, os, typing, asyncio, re, datetime, json, dataclasses (bientôt)

Ce que Monty ne peut pas faire :

  • Utiliser le reste de la bibliothèque standard
  • Utiliser des bibliothèques tierces (comme Pydantic) ; la prise en charge de bibliothèques Python externes n'est pas un objectif
  • définir des classes (la prise en charge devrait arriver bientôt)
  • utiliser les instructions match (encore une fois, la prise en charge devrait arriver bientôt)

En bref, Monty est extrêmement limité et conçu pour un seul cas d'usage :

Exécuter du code écrit par des agents.

Pour comprendre pourquoi vous pourriez vouloir faire cela, consultez :

En termes très simples, l'idée de tout ce qui précède est que les LLM peuvent travailler plus vite, à moindre coût et de manière plus fiable si on leur demande d'écrire du code Python (ou Javascript) au lieu de s'appuyer sur l'appel d'outils traditionnel. Monty rend cela possible sans la complexité d'un sandbox ni le risque d'exécuter du code directement sur l'hôte.

Remarque : Monty sera (bientôt) utilisé pour implémenter codemode dans Pydantic AI

Utilisation

Monty peut être appelé depuis Python, JavaScript/TypeScript ou Rust.

Python

Pour installer :```bash uv add pydantic-monty

(Ou `pip install pydantic-monty` pour les boomers)

`pydantic-monty` est un méta-paquet associant `pydantic-monty-client` (le module `pydantic_monty`) avec `pydantic-monty-runtime` (le binaire worker `monty`). Installez `pydantic-monty-client` seul si le binaire provient déjà d'ailleurs.

Utilisation :```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'exécution a lieu dans un pool de sous-processus de travail monty, donc même une erreur mémoire déclenchée par du code hostile (débordement de pile, arrêt de l'allocateur) ne peut jamais faire planter votre processus — le travailleur meurt, lève MontyCrashedError, et est remplacé. Il existe également une API entièrement synchrone :```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

Pour installer :```bash
npm install @pydantic/monty

Le package JS est un binding natif (napi) sur le même pool de workers Rust que le package Python utilise — le binding et le binaire worker monty sont fournis via des packages npm spécifiques à la plateforme :```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' }, })

Pour les navigateurs (ou partout où les sous-processus sont impossibles), le même paquet
expose une version WebAssembly en processus sous le sous-chemin `@pydantic/monty/wasm`
(pas d'isolation en cas de crash : un crash de la sandbox y est un crash de l'hôte).

### Rust

Pour exécuter du code non fiable depuis Rust, nous recommandons la
crate [`monty-pool`](https://crates.io/crates/monty-pool) plutôt que l'API en processus ci-dessous.
`monty-pool` n'exécute le code que dans des sous-processus worker `monty`, ce qui offre des protections supplémentaires :
un crash déclenché par du code adversaire (dépassement de pile, arrêt de l'allocateur) ne tue que le worker —
le pool détecte la mort et remplace le worker — et un chien de garde côté parent peut tuer les workers
qui dépassent un délai d'attente strict. C'est le même moteur que celui sur lequel les paquets Python
et JavaScript ci-dessus sont construits. Voir le [README de monty-pool](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
pour l'utilisation.

La crate `monty` elle-même fournit l'interpréteur en processus :```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));

Sérialisation

Une session REPL peut être sérialisée avec dump() et restaurée avec Dump::load(). Le dump transporte les métadonnées de session (nom du script, stubs de vérification de type) ainsi que l'état de l'interpréteur, sous une version que la build de chargement vérifie :```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` et `RunProgress` n'ont pas de format de dump propre, mais implémentent tous deux `serde::Serialize`/`Deserialize`, si bien qu'un hôte peut sérialiser du code analysé ou une exécution suspendue avec le format qu'il utilise déjà.

## Limites de mémoire dans les workers

La `max_memory` d'une session est mesurée par l'allocateur du worker. L'interpréteur
signale proprement une `MemoryError` après avoir franchi la limite souple ; une limite
stricte plus élevée tue et remplace le worker si une allocation saute trop loin entre deux points de contrôle.

Voir [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) pour savoir comment
le dépassement d'une limite se manifeste pour un hôte, et `monty-alloc` pour l'allocateur sous
lequel s'exécutent les workers du sous-processus et WebAssembly.

## Intégration PydanticAI

Monty propulsera le mode code dans
[Pydantic AI](https://github.com/pydantic/pydantic-ai). Au lieu d'effectuer des
appels d'outils séquentiels, le LLM écrit du code Python qui appelle vos outils
comme fonctions et Monty l'exécute en toute sécurité.```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 - bindings Go pour l'interpréteur Monty
  • Dart/Flutter : dart_monty (github) (pub.dev)- bindings Dart/Flutter pour Monty

Alternatives

Il y a généralement deux réactions lorsqu'on montre Monty à des gens :

  1. Oh mon Dieu, ça résout tellement de problèmes, j'en veux.
  2. Pourquoi pas X ?

Où X est une technologie alternative. Curieusement, ces réactions sont souvent combinées, ce qui suggère que les gens n'ont pas encore trouvé une alternative qui leur convient, mais sont incrédules qu'il n'existe vraiment pas de bonne alternative à la création d'une implémentation Python entièrement nouvelle.

Je vais passer en revue les alternatives les plus évidentes et expliquer pourquoi elles ne convenaient pas à ce que nous voulions.

NOTE: toutes ces technologies sont impressionnantes et ont des usages répandus, ce commentaire sur leurs limites pour notre cas d'usage ne doit pas être perçu comme une critique. La plupart de ces solutions n'ont pas été conçues dans le but de fournir un sandbox pour LLM, c'est pourquoi elles n'y sont pas nécessairement excellentes.

TechnologieComplétude du langageSécuritéLatence de démarrageFOSSComplexité de configurationMontage de fichiersInstantanés
Montypartiellestricte0.06msgratuit / OSSfacilefacilefacile
Dockercomplètebonne195msgratuit / OSSintermédiairefacileintermédiaire
Pyodidecomplètemauvaise2800msgratuit / OSSintermédiairefaciledifficile
starlark-rusttrès limitéebonne1.7msgratuit / OSSfacilenon disponible ?impossible ?
WASI / Wasmerpartielle, presque complètestricte66msgratuit *intermédiairefacileintermédiaire
service de sandboxcomplètestricte1033msnon gratuitintermédiairedifficileintermédiaire
YOLO Pythoncomplèteinexistante0.1ms / 30msgratuit / OSSfacilefacile / effrayantdifficile

Voir ./scripts/startup_performance.py pour le script utilisé pour calculer les chiffres de performance au démarrage.

Détails sur chaque ligne ci-dessous :

Monty

  • Complétude du langage : Pas de classes (pour l'instant), stdlib limitée, pas de bibliothèques tierces
  • Sécurité : Accès au système de fichiers, au réseau et à l'environnement explicitement contrôlé, limites strictes sur le temps d'exécution et l'utilisation de la mémoire
  • Latence de démarrage : Démarre en microsecondes
  • Complexité de configuration : juste pip install pydantic-monty ou npm install @pydantic/monty, téléchargement d'environ 4.5 Mo
  • Montage de fichiers : Strictement contrôlé, voir #85
  • Instantanés : La fonctionnalité de pause et de reprise de Monty avec dump() et load() rend triviale la pause, la reprise et la duplication (fork) de l'exécution.

Docker

  • Complétude du langage : CPython complet avec n'importe quelle bibliothèque
  • Sécurité : Isolation des processus et du système de fichiers, politiques réseau, mais des évasions de conteneur existent, la limitation de mémoire est possible
  • Latence de démarrage : Surcharge de démarrage du conteneur (~195ms mesurés)
  • Complexité de configuration : Nécessite le démon Docker, des images de conteneur, de l'orchestration, python:3.14-alpine pèse 50 Mo - docker ne peut pas être installé depuis PyPI
  • Montage de fichiers : Les montages de volumes fonctionnent bien
  • Instantanés : Possibles avec des solutions d'exécution durable comme Temporal, ou en créant un instantané d'une image et en l'enregistrant comme image Docker.

Pyodide

  • Complétude du langage : CPython complet compilé en WASM, presque toutes les bibliothèques disponibles
  • Sécurité : Repose sur le sandbox du navigateur/WASM - non conçu pour l'isolation côté serveur, le code Python peut exécuter du code arbitraire dans le runtime JS, seul deno permet l'isolation, les limites de mémoire sont difficiles/impossibles à appliquer avec deno
  • Latence de démarrage : Le chargement du runtime WASM est lent (~2800ms à froid)
  • Complexité de configuration : Il faut charger le runtime WASM, gérer l'initialisation asynchrone, le paquet NPM pyodide pèse ~12 Mo, deno ~50 Mo - Pyodide ne peut pas être appelé uniquement avec des paquets PyPI
  • Montage de fichiers : Système de fichiers virtuel via les API du navigateur
  • Instantanés : Possibles avec des solutions d'exécution durable comme Temporal probablement, mais difficiles

starlark-rust

Voir starlark-rust.

  • Complétude du langage : Langage de configuration, pas Python - pas de classes, exceptions, async
  • Sécurité : Déterministe et hermétique par conception
  • Latence de démarrage : s'exécute embarqué dans le processus comme Monty, d'où un temps de démarrage impressionnant
  • Complexité de configuration : Utilisable en Python via starlark-pyo3
  • Montage de fichiers : Aucune gestion de fichiers par conception AFAIK ?
  • Instantanés : Impossible AFAIK ?

WASI / Wasmer

Exécuter Python dans WebAssembly via Wasmer.

  • Complétude du langage : CPython complet, les paquets externes purement Python fonctionnent via le montage, les paquets externes avec des bindings C ne fonctionnent pas
  • Sécurité : En principe, WebAssembly devrait fournir de fortes garanties de sandboxing.
  • Latence de démarrage : Le paquet Python wasmer n'a pas été mis à jour depuis 3 ans et je n'ai pas trouvé de documentation sur l'appel de Python dans wasmer depuis Python, je l'ai donc appelé via subprocess. La latence de démarrage était de 66ms.
  • Complexité de configuration : Le téléchargement de wasmer fait 100 Mo, le paquet « python/python » fait 50 Mo.
  • FOSS : J'ai marqué ceci comme « gratuit * » puisque le coût est nul mais tout ne semble pas être open source. En date du 2026-02-10, le paquet python/python wasmer n'a pas de readme, pas de licence, pas de lien source et aucune indication sur la façon dont il est construit ; les versions récemment téléversées affichent une taille « 0B » alors que le téléchargement fait ~50 Mo - le processus de construction du binaire Python n'est ni clair ni transparent. (Si je me trompe, veuillez créer une issue pour me corriger)
  • Montage de fichiers : Pris en charge
  • Instantanés : Pris en charge via la journalisation

service de sandbox

Des services comme Daytona, E2B, Modal.

Il y a des défis similaires, plus de complexité de configuration mais une latence réseau plus faible pour mettre en place votre propre configuration sandbox avec k8s.

  • Complétude du langage : CPython complet avec n'importe quelle bibliothèque
  • Sécurité : Isolation de conteneur gérée professionnellement
  • Latence de démarrage : Aller-retour réseau et temps de démarrage du conteneur. J'ai obtenu ~1s de démarrage à froid avec Daytona EU depuis Londres ; Daytona annonce une latence sous 90ms, probablement pour un conteneur existant, on ne sait pas si cela inclut la latence réseau.
  • FOSS : Paiement par exécution ou par temps de calcul, certaines implémentations sont open source
  • Complexité de configuration : Intégration API, jetons d'authentification - convenable pour les startups mais généralement non envisageable pour les entreprises
  • Montage de fichiers : Téléversement/téléchargement via des appels API
  • Instantanés : Possibles avec des solutions d'exécution durable comme Temporal ; les services offrent également certaines solutions pour cela, je pense basées sur des conteneurs Docker

YOLO Python

Exécuter Python directement via exec() (~0.1ms) ou subprocess (~30ms).

  • Complétude du langage : CPython complet avec n'importe quelle bibliothèque
  • Sécurité : Aucune - accès complet au système de fichiers, réseau, variables d'environnement, commandes système
  • Latence de démarrage : Quasi nulle pour exec(), ~30ms pour subprocess
  • Complexité de configuration : Aucune
  • Montage de fichiers : Accès direct au système de fichiers (c'est le problème)
  • Instantanés : Possibles avec des solutions d'exécution durable comme Temporal

Partie du Pydantic Stack

Le Pydantic Stack est tout ce dont vous avez besoin pour livrer des agents IA de qualité production :

Catégories