dart_monty_core

Run Python in Dart. A thin binding for pydantic/monty — the sandboxed Python interpreter from Pydantic, written in Rust.

dart_monty_core is the raw binding layer — Monty, MontyRepl, MontyValue, the FFI/WASM platform glue. For a higher-level API (Flutter integration, asset auto-loading, plugin scaffolding) see dart_monty, which depends on this package.

Pre-1.0 — install via git: from GitHub (see Installation below). Versioning convention: minor version mirrors the upstream monty patch (0.X.0 ↔ monty v0.0.X).

Why

Dart is compiled, no reflection — fast and tree-shakeable, but you can't ship new behaviour without re-shipping a binary. Monty is a sandboxed Python runtime designed to behave as Dart's scripting language: an embeddable Python subset under hard resource limits, on both native (FFI) and web (WASM).

LLMs generate excellent Python. Let them script your Dart app — through code your app type-checks, runs in a sandbox, exposes only the external functions and OS calls you whitelist, and inspects the typed result. More flexible than a plug-in registry, safer than eval — Pydantic runs an active $5,000 bug bounty at hackmonty.com for the underlying interpreter.

final errors = await Monty.typeCheck(llmCode);
if (errors.isNotEmpty) return;

final result = await Monty(llmCode).run(
  inputs: {'temperatureC': 22},
  externalFunctions: {
    'fetchWeather': (args) async => weatherApi.get(args['_0'] as String),
    'log': (args) async { logger.info(args['_0']); return null; },
  },
  limits: const MontyLimits(memoryBytes: 32 << 20, timeoutMs: 5000),
);

Quick start

import 'package:dart_monty_core/dart_monty_core.dart';

// One-shot
final r = await Monty.exec('2 ** 10');
print(r.value); // MontyInt(1024)

// Compiled program — different inputs, no shared state
final program = Monty('x * y');
print((await program.run(inputs: {'x': 10, 'y': 3})).value); // MontyInt(30)
print((await program.run(inputs: {'x': 7, 'y': 6})).value);  // MontyInt(42)

// Stateful REPL — variables, functions, imports survive
final repl = MontyRepl();
await repl.feedRun('def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)');
print((await repl.feedRun('fib(10)')).value); // MontyInt(55)
await repl.dispose();

API

Monty — compiled program

Monty(code, {scriptName}) Hold source as a re-runnable program
run({inputs, externalFunctions, limits, osHandler, printCallback}) Run in a fresh interpreter
Monty.exec(code, {…}) One-shot wrapper
Monty.compile(code) / Monty.runPrecompiled(bytes, {…}) Pre-compile and replay
Monty.typeCheck(code, {prefixCode, scriptName}) Static type analysis → List<MontyTypingError>

MontyRepl — stateful REPL

MontyRepl({scriptName, preamble}) Auto-detected backend
feedRun(code, {inputs, externalFunctions, osHandler, printCallback}) State persists
feedStart(code) + resume / resumeWithError Iterative externals + OS calls
detectContinuation(code) >>> vs ... mode
snapshot() / restore(bytes) Serialise / restore the heap
clearState() / dispose() Wipe / free

Multiple MontyRepls coexist — each owns its own Rust heap.

MontyValue — typed Python values

switch (result.value) {
  case MontyInt(:final value):     /* … */ ;
  case MontyString(:final value):  /* … */ ;
  case MontyList(:final items):    /* … */ ;
  case MontyDict(:final entries):  /* … */ ;
  case MontyDate(:final year):     /* … */ ;
  case MontyNamedTuple(:final fieldNames, :final values): /* … */ ;
  case MontyDataclass(:final name, :final attrs): /* … */ ;
  case MontyNone(): /* … */ ;
}

18 subtypes — scalars (MontyInt, MontyFloat, MontyString, MontyBool, MontyBytes, MontyNone), collections (MontyList, MontyTuple, MontyDict, MontySet, MontyFrozenSet), datetime (MontyDate, MontyDateTime, MontyTimeDelta, MontyTimeZone), and structured (MontyPath, MontyNamedTuple, MontyDataclass). MontyDataclass.hydrate(factory) turns a Python @dataclass into your own Dart class:

final user = (result.value as MontyDataclass).hydrate(User.fromAttrs);

Build from Dart with MontyValue.fromDart(value).

Errors

MontySyntaxError Python parse error (subtype of MontyScriptError)
MontyScriptError Python runtime exception
MontyResourceError Limit exceeded (memory / stack / timeout)

run() / feedRun() surface Python-level exceptions in MontyResult.error rather than throwing — the interpreter stays alive. Resource limits and disposal still throw.

External functions

Python calls Dart callbacks by name. Positional args at _0, _1, …; kwargs by Python name. Sync or async.

await Monty('compute("mul", 6, 7)').run(externalFunctions: {
  'compute': (args) async => switch (args['_0']) {
    'mul' => (args['_1'] as int) * (args['_2'] as int),
    _ => 0,
  },
});

OS calls

pathlib, os.getenv, datetime.now, time.time pause and call your OsCallHandler. Optional — provide only when the script touches the OS.

await Monty('os.getenv("HOME")').run(
  osHandler: (op, args, kwargs) async => switch (op) {
    'os.getenv' => Platform.environment[args[0] as String],
    _ => throw OsCallException('not supported',
        pythonExceptionType: 'PermissionError'),
  },
);

memoryMountedOsHandler (lib/src/mount/) provides a ready-made in-memory VFS with mount-based sandboxing.

Resource limits

await Monty(code).run(
  limits: const MontyLimits(
    memoryBytes: 32 << 20,
    stackDepth: 200,
    timeoutMs: 5000,
  ),
);

JS-aligned spelling: MontyLimits.jsAligned(maxMemory:, maxDurationSecs:, maxRecursionDepth:).

Backends

Selected when
MontyFfi dart.library.ffi present (desktop / server / mobile)
MontyWasm dart.library.js_interop present (web)
createPlatformMonty() Auto-pick at compile time

Installation

0.17.0 builds the native FFI binary from source on dart pub get. Every FFI consumer needs a Rust toolchain, including Flutter consumers coming in via dart_monty.

Prebuilt binaries arrive in 0.17.1 for macOS (arm64+x86_64), Linux (x86_64-gnu+aarch64-gnu), Windows (x86_64), iOS (xcframework), and Android (4 ABIs). The build hook will download the matching artefact from this repo's GitHub Releases on first pub get — no Rust toolchain required. See AGENTS.md "Native binary release pipeline (0.17.1+)".

Install (from GitHub)

dart_monty_core is distributed via GitHub. Do not use dart pub add dart_monty_core — pub.dev does not yet have 0.17.0; the historical dart_monty 0.11.0 there is a different, older API.

dependencies:
  dart_monty_core:
    git:
      url: https://github.com/runyaga/dart_monty_core.git
      ref: main

For local development against a worktree, use path: instead:

dependencies:
  dart_monty_core:
    git:
      url: https://github.com/runyaga/dart_monty_core
      ref: v0.17.0   # pin to a tag; do not float on main

Prerequisites for FFI (desktop only)

hook/build.dart runs cargo build --release --target <host-triple> on the consumer's machine during pub get. Required toolchain:

  • Rust — install via rustup
  • C linker for the cdylib link step:
    • macOS: xcode-select --install (provides clang)
    • Linux: sudo apt install build-essential / dnf install gcc / equivalent
    • Windows: Visual Studio Build Tools with the C++ workload

Supported FFI host triples in v0.17.0: aarch64-apple-darwin, x86_64-apple-darwin, aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu, aarch64-pc-windows-msvc, x86_64-pc-windows-msvc. Mobile (iOS, Android) is not handled by this package's hook — the hook returns no native asset for those targets. If you're using dart_monty_core directly and need Monty on mobile, compiling and wiring the native crate into your Flutter project's iOS / Android plugin is your responsibility. For a higher-level Flutter integration, use dart_monty.

First pub get takes 1–3 minutes (compiling the native crate); subsequent runs reuse cargo's cache.

Web (WASM)

WASM ships pre-built — no toolchain required. Copy the three assets into your web/ and add a script tag:

# Git-deps cache the cloned repo here (path encodes the resolved sha):
SRC=$(find ~/.pub-cache/git -maxdepth 2 -type d -name 'dart_monty_core-*' | head -1)
cp "$SRC/lib/assets/dart_monty_core_bridge.js" web/
cp "$SRC/lib/assets/dart_monty_core_worker.js" web/
cp "$SRC/lib/assets/dart_monty_core_native.wasm" web/
<script src="dart_monty_core_bridge.js"></script>

packages/dart_monty_web/ in this repo demonstrates the full wiring.

Other ecosystems

  • Flutterdart_monty wraps this package with the Flutter integration layer (asset loading, plugin scaffolding). When using dart_monty_core directly, mobile (iOS / Android) compilation is your responsibility; dart_monty is the alternative.
  • JS / TS — use @pydantic/monty; the canonical npm package.

Known upstream limitations

External functions can't be called from inside iterator-consuming C builtins — map(ext_fn, …), filter(ext_fn, …), sorted(…, key=ext_fn) raise RuntimeError upstream. First-class references work everywhere else.

License

MIT.

Libraries

dart_monty_core
dart_monty_core — thin Dart binding for pydantic/monty.