dart_monty 0.10.0
dart_monty: ^0.10.0 copied to clipboard
Pure Dart bindings for Monty, a restricted sandboxed Python interpreter built in Rust. Run Python from Dart and Flutter on desktop, web, and mobile.
dart_monty #
Monty is a restricted, sandboxed Python interpreter built in Rust by the Pydantic team. It runs a safe subset of Python designed for embedding.
This package is co-designed by human and AI — nearly all code is AI-generated.
dart_monty provides pure Dart bindings for the Monty interpreter, bringing sandboxed Python execution to Dart and Flutter apps — on desktop, web, and mobile — with resource limits, iterative execution, and snapshot/restore support.
Fork notice: dart_monty currently builds against
runyaga/monty(branchrunyaga/main), a fork ofpydantic/monty. The fork carries patches required for embedding that are not yet upstream. We intend to upstream all changes and return topydantic/montyonce accepted.
Patch Fork PR Upstream PR Status Why needed Fix partial future resolution panics in mixed asyncio.gather()— pydantic/monty#251 Submitted, awaiting review Two panics in async_exec.rswhen a gather mixes coroutine tasks with direct external calls — blocks any async host function useCancellableTracker (preemptive script cancellation) runyaga/monty#3 Not yet submitted Merged in fork Cooperative cancel via Arc<AtomicBool>checked in bytecode loop — required fordispose()to not hang on stuck FFI callscpu: wasm32restriction inmonty-wasm32-wasinpm package— runyaga/monty#4 Open issue npm refuses install on non-wasm hosts, blocking CI and local dev
Platform Support #
| Platform | Status |
|---|---|
| macOS | Supported |
| Linux | Supported |
| Web (browser) | Supported |
| Windows | Planned |
| iOS | Planned |
| Android | Planned |
Quick Start #
1. Add the dependency
dart pub add dart_monty
2. Write three lines
import 'package:dart_monty/dart_monty.dart';
void main() async {
final monty = Monty();
final result = await monty.run('2 + 2');
print(result.value); // 4
await monty.dispose();
}
3. Run it
$ dart run
Result: 4
No Flutter. No bindings. No registration. It just works.
Web Quick Start #
The same Dart code runs in the browser — Monty() selects the WASM backend
at compile time. You need three asset files and COOP/COEP headers.
1. Build the WASM binary and JS bridge
# Build the Rust WASM binary
cd native && cargo build --release --target wasm32-wasip1
# Build the JS bridge and worker
cd packages/dart_monty_wasm/js && npm install && npm run build
2. Copy assets into your web directory
cp packages/dart_monty_wasm/assets/dart_monty_bridge.js web/
cp packages/dart_monty_wasm/assets/dart_monty_worker.js web/
cp packages/dart_monty_wasm/assets/dart_monty_native.wasm web/
3. Write your Dart code (same API as native)
import 'package:dart_monty/dart_monty.dart';
void main() async {
final monty = Monty();
final result = await monty.run('2 + 2');
print(result.value); // 4
await monty.dispose();
}
4. Compile and serve
dart compile js bin/main.dart -o web/main.dart.js
Your HTML loads the bridge before the compiled Dart:
<script src="dart_monty_bridge.js"></script>
<script src="main.dart.js"></script>
Serve with COOP/COEP headers (required for SharedArrayBuffer):
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Each WASM session uses ~16 MB of memory. Worker.terminate() provides
preemptive cancellation — no stuck scripts.
Usage #
import 'package:dart_monty/dart_monty.dart';
final monty = Monty();
// Simple execution
final result = await monty.run('2 + 2');
print(result.value); // 4
// With resource limits
final limited = await monty.run(
'sum(range(100))',
limits: const MontyLimits(timeoutMs: 5000, memoryBytes: 10 * 1024 * 1024),
);
print(limited.value); // 4950
await monty.dispose();
External Functions #
When Python calls a function listed in externalFunctions, execution
pauses and Dart handles the call. The function name in Python maps 1:1
to the name you provide — when Python calls fetch(...), Dart receives
a MontyPending with functionName == 'fetch' and the arguments Python
passed.
// Python calls fetch() → execution pauses → Dart handles it → resumes
var progress = await monty.start(
'fetch("https://api.example.com/users")',
externalFunctions: ['fetch'],
);
// Dispatch loop: match functionName to your Dart implementation
while (progress is MontyPending) {
final pending = progress as MontyPending;
final name = pending.functionName; // 'fetch'
final args = pending.arguments; // ['https://api.example.com/users']
switch (name) {
case 'fetch':
final url = args.first as String;
final response = await http.get(Uri.parse(url));
progress = await monty.resume(jsonDecode(response.body));
default:
progress = await monty.resumeWithError(
'Unknown function: $name',
);
}
}
final complete = progress as MontyComplete;
print(complete.result.value);
await monty.dispose();
Bridge and Plugin System #
The start()/resume() loop above is the low-level platform interface.
For real applications, dart_monty_bridge provides a higher-level API
that handles the dispatch loop, argument coercion, and event streaming
automatically.
DefaultMontyBridge wraps the dispatch loop and emits a
Stream<BridgeEvent> — tool calls, text output, and lifecycle events:
import 'package:dart_monty_bridge/dart_monty_bridge.dart';
final bridge = DefaultMontyBridge(platform: Monty());
// Register host functions directly on the bridge
bridge.register(HostFunction(
schema: const HostFunctionSchema(
name: 'get_price',
description: 'Get stock price by ticker symbol.',
params: [HostParam(name: 'symbol', type: HostParamType.string)],
),
handler: (args) async => 42.50,
));
// Execute — bridge handles the dispatch loop for you
await for (final event in bridge.execute('price = get_price("AAPL")')) {
switch (event) {
case BridgeToolCallResult(:final name, :final result):
print('$name returned: $result');
case BridgeTextContent(:final delta):
print('Output: $delta');
case BridgeRunFinished():
print('Done');
default:
break;
}
}
await bridge.dispose();
MontyPlugin groups related host functions under a validated namespace.
PluginRegistry collects plugins with collision detection and wires
them onto a bridge:
class WeatherPlugin extends MontyPlugin {
@override
String get namespace => 'weather';
@override
List<HostFunction> get functions => [
HostFunction(
schema: const HostFunctionSchema(
name: 'weather_forecast',
description: 'Get weather forecast for a city.',
params: [HostParam(name: 'city', type: HostParamType.string)],
),
handler: (args) async => {'temp': 72, 'condition': 'sunny'},
),
];
}
final registry = PluginRegistry()..register(WeatherPlugin());
await registry.attachTo(bridge); // registers functions + introspection builtins
Plugins enforce namespace_ prefixes on function names (e.g., weather_forecast),
provide lifecycle hooks (onRegister, onDispose), and auto-generate
list_functions / help introspection builtins so Python code can discover
available tools at runtime.
Error Handling #
dart_monty uses a sealed MontyError hierarchy for structured error handling:
import 'package:dart_monty_platform_interface/dart_monty_platform_interface.dart';
try {
final result = await monty.run('1 / 0');
} on MontyError catch (e) {
switch (e) {
case MontyScriptError(:final exception):
print('Python error: ${exception.message}');
print('Type: ${exception.excType}');
for (final frame in exception.traceback) {
print(' ${frame.filename}:${frame.lineNumber} in ${frame.name}');
}
case MontyCancelledError():
print('Execution was cancelled');
case MontyResourceError(:final exception):
print('Resource limit exceeded: ${exception.message}');
case MontyPanicError(:final message):
print('Interpreter panic: $message');
case MontyCrashError(:final message):
print('Interpreter crash: $message');
case MontyDisposedError():
print('Interpreter was disposed');
}
}
Cancellation #
Cancel a running interpreter from any isolate using MontyCancelToken:
final token = MontyCancelToken(handleId);
token.cancel(); // sets atomic flag in bytecode loop
Stateful Sessions #
MontySession persists Python globals across multiple run() calls using
snapshot/restore under the hood:
import 'package:dart_monty_platform_interface/dart_monty_platform_interface.dart';
final session = MontySession(platform: Monty());
// Globals persist across run() calls via snapshot/restore
await session.run('x = 42');
await session.run('y = x * 2');
final result = await session.run('x + y');
print(result.value); // 126
// Session also supports start/resume (same dispatch pattern)
await session.clearState();
await session.dispose();
Monty API Coverage (~75%) #
dart_monty wraps the Monty Rust API (fork of pydantic/monty). The table below shows current coverage and what's planned.
| API Area | Status | Notes |
|---|---|---|
Core execution (run, start, resume, dispose) |
Covered | Full iterative execution loop |
| External functions (host-provided callables) | Covered | start() / resume() / resumeWithError() |
| Resource limits (time, memory, recursion depth) | Covered | MontyLimits on run() and start() |
Print capture (print() output collection) |
Covered | MontyResult.printOutput |
Snapshot / restore (MontyRun::dump/load) |
Covered | Compile-once, run-many pattern |
| Exception model (excType, traceback, stack frames) | Covered | Full MontyException with StackFrame list |
| Call metadata (kwargs, callId, methodCall, scriptName) | Covered | Structured external call context |
| Cancellation (cooperative abort via atomic flag) | Covered | MontyCancelToken, cancel(), terminate() with zombie tracking |
Error hierarchy (sealed MontyError with 6 subtypes) |
Covered | Script, Cancel, Panic, Crash, Disposed, Resource |
| Multi-session (WASM Worker pool) | Covered | createSession/disposeSession, 16 MB per session |
Async / futures (asyncio.gather, concurrent calls) |
Covered | resumeAsFuture(), resolveFutures() on both FFI and WASM |
| Rich types (tuple, set, bytes, dataclass, namedtuple) | Planned | Currently collapsed to List/Map |
REPL (stateful sessions, feed(), persistence) |
Planned | MontyRepl multi-step sessions |
OS calls (os.getenv, os.environ, os.stat) |
Planned | MontyPlugin host functions |
| Print streaming (real-time callback) | Planned | Currently batch-only after execution |
Advanced limits (allocations, GC interval, runNoLimits) |
Planned | Extended ResourceTracker surface |
| Type checking (static analysis before execution) | Planned | ty / Red Knot integration |
| Progress serialization (suspend/resume across restarts) | Planned | RunProgress::dump/load |
| Platform expansion (Windows, iOS, Android) | Planned | macOS + Linux + Web today |
Architecture #
See docs/architecture.md for detailed architecture documentation including state machine contracts, memory management, error handling, and cross-backend parity guarantees.
dart_monty selects the native or web backend at compile time via conditional imports — no Flutter required. Four pure-Dart packages:
| Package | Description |
|---|---|
dart_monty |
App-facing API — Monty() convenience class with compile-time backend selection |
dart_monty_bridge |
High-level bridge — DefaultMontyBridge, BridgeEvent streams, MontyPlugin / PluginRegistry |
dart_monty_platform_interface |
Abstract contract (MontyPlatform), shared types, SPI for backend authors |
dart_monty_ffi |
Native FFI bindings (dart:ffi -> Rust shared library) |
dart_monty_wasm |
WASM bindings (dart:js_interop -> Web Worker) |
All packages are pure Dart and work in CLI tools, server-side Dart, and Flutter apps alike.
Native Path (desktop) #
Dart app -> Monty() -> MontyFfi (dart:ffi)
-> libdart_monty_native.{dylib,so}
-> Monty Rust interpreter
Web Path (browser) #
Dart app (compiled to JS) -> Monty() -> MontyWasm (dart:js_interop)
-> Web Worker -> @pydantic/monty WASM
The Web Worker architecture bypasses Chrome's 8 MB synchronous WASM compilation limit.
Contributing #
See CONTRIBUTING.md for development setup, gate scripts, and CI details.
License #
MIT License. See LICENSE.