How to Use Monty from JavaScript/TypeScript with napi-rs
You can use Monty from JavaScript/TypeScript by installing the @pydantic/monty npm package, which exposes a Rust-based sandboxed Python interpreter through napi-rs bindings that support synchronous, iterative, and asynchronous execution patterns.
The @pydantic/monty package provides a high-performance bridge between Node.js and the Monty sandboxed Python interpreter. When you use Monty from JavaScript/TypeScript with napi-rs, you get type-safe access to Python execution with automatic data conversion, resource limits, and support for external JavaScript functions.
What Is the Monty JavaScript/TypeScript Bridge?
The napi-rs layer in @pydantic/monty wraps the Rust-implemented Monty interpreter and exposes it to Node.js through a thin, efficient binding layer. This architecture allows you to execute untrusted Python code safely while maintaining native JavaScript ergonomics.
| Component | Role | Source |
|---|---|---|
Monty class |
Parses Python code once, then runs it (or runs it iteratively) with optional inputs, resource limits, and external callbacks. | crates/monty-js/src/monty_cls.rs |
MontySnapshot / MontyComplete |
Represent a paused external-function call (MontySnapshot) and a finished run (MontyComplete). |
crates/monty-js/src/monty_cls.rs |
| Conversion layer | Bidirectional conversion between Rust MontyObject and JavaScript values (including Map, Set, Buffer, BigInt, etc.). |
crates/monty-js/src/convert.rs |
runMontyAsync helper |
Executes code that may perform await-style external functions. | crates/monty-js/src/lib.rs |
| Error types | MontySyntaxError, MontyRuntimeError, MontyTypingError map Python-side errors to JavaScript exceptions. |
crates/monty-js/src/exceptions.rs |
| Resource-limit struct | JsResourceLimits lets the host cap allocations, runtime, memory, and recursion depth. |
crates/monty-js/src/limits.rs |
Installation and Basic Setup
Install the package via npm or your preferred package manager:
npm install @pydantic/monty
Import the main classes and helpers in your TypeScript or JavaScript file:
import { Monty, runMontyAsync, JsResourceLimits } from '@pydantic/monty'
Core API Components
The Monty Class
The Monty class is the primary entry point defined in crates/monty-js/src/monty_cls.rs. When you instantiate new Monty(code, options?), the constructor invokes Monty::create in Rust, which parses the Python source and optionally runs static type checking.
Key methods include:
run(options?): Executes the parsed code synchronously.start(): Begins iterative execution, returning aMontySnapshotwhen the code hits an external function.dump(): Serializes the compiled Monty instance to aBufferfor caching.load(buffer): Static method to restore a previously dumped instance.
Execution Results: MontySnapshot and MontyComplete
When using iterative execution via start() and resume(), the runtime returns specific objects:
MontySnapshot: Represents a paused execution state when Python calls an external JavaScript function. It stores the function name, positional/keyword arguments (asMontyObjectvalues), and an optional print-callback reference. Callsnapshot.resume({returnValue, exception})to continue execution.MontyComplete: Represents a finished execution. Access the final result via the.outputgetter.
Error Handling
The napi-rs bridge maps Python-side errors to specific JavaScript exception classes defined in crates/monty-js/src/exceptions.rs:
MontySyntaxError: Raised when Python code fails to parse.MontyRuntimeError: Raised for runtime exceptions (e.g., NameError, TypeError) or when resource limits are exceeded.MontyTypingError: Raised when static type checking is enabled and fails.
All errors wrap the internal JsMontyException struct to preserve stack traces and error messages across the FFI boundary.
Resource Limits
The JsResourceLimits interface (defined in crates/monty-js/src/limits.rs) allows you to sandbox Python execution by capping:
maxAllocations: Maximum number of object allocations.maxDurationSecs: Maximum execution time in seconds.maxMemory: Maximum memory usage in bytes.maxRecursionDepth: Maximum Python call stack depth.
Pass these limits to the run() or start() methods via the limits option.
Execution Patterns
Synchronous Execution with run()
For simple scripts without external functions or async operations, use the synchronous run() method. This path invokes MontyRun::run directly in Rust after converting inputs via Monty::extract_input_values.
import { Monty } from '@pydantic/monty'
const m = new Monty('1 + 2')
const result = m.run() // → 3
Iterative Execution with start() and resume()
When your Python code calls external JavaScript functions, use iterative execution to handle the calls manually. The start() method begins execution and returns a MontySnapshot whenever Python invokes an external function.
import { Monty, MontySnapshot, MontyComplete } from '@pydantic/monty'
const m = new Monty('add(a, b)', { externalFunctions: ['add'] })
let prog = m.start()
while (prog instanceof MontySnapshot) {
console.log(`Calling ${prog.functionName}`)
prog = prog.resume({ returnValue: 10 })
}
if (prog instanceof MontyComplete) {
console.log('Result:', prog.output)
}
Asynchronous Execution with runMontyAsync()
Because run() cannot suspend for async work, the package exports runMontyAsync(montyInstance, runOptions) from crates/monty-js/src/lib.rs. This helper internally uses Monty::start and repeatedly awaits Promises that resolve when snapshots are ready, enabling idiomatic await patterns from JavaScript.
import { Monty, runMontyAsync } from '@pydantic/monty'
const code = `
async def fetch_data(url):
return await external_fetch(url)
result = await fetch_data("https://example.com")
`
const m = new Monty(code, {
externalFunctions: ['external_fetch']
})
const out = await runMontyAsync(m, {
externalFunctions: {
external_fetch: async (url: string) => {
const resp = await fetch(url)
return resp.text()
}
}
})
Data Type Conversion Between JavaScript and Python
The conversion layer in crates/monty-js/src/convert.rs handles bidirectional translation between Rust MontyObject and JavaScript values. This ensures seamless data passing without manual serialization.
Supported conversions include:
- Objects and Primitives: JavaScript objects and primitives map directly to Python dicts and scalars.
- Map and Set: Native JavaScript
MapandSetinstances convert to Pythondictandsetequivalents. - Buffer: Node.js
Bufferobjects translate to Pythonbytes. - BigInt: JavaScript
BigIntvalues map to Python integers without precision loss.
When Python code returns values, the monty_to_js function in the conversion layer transforms Rust MontyObject back to appropriate JavaScript types.
Advanced Usage Examples
Providing Input Variables
Pass variables to Python code by declaring them in the constructor and supplying values at runtime:
import { Monty } from '@pydantic/monty'
const m = new Monty('x * y', { inputs: ['x', 'y'] })
const result = m.run({ inputs: { x: 7, y: 6 } }) // → 42
The Monty::extract_input_values method in crates/monty-js/src/monty_cls.rs handles the conversion from JavaScript objects to Python scope variables.
External Functions (Synchronous)
Register JavaScript functions that Python code can call during execution:
import { Monty } from '@pydantic/monty'
const m = new Monty('add(a, b)', { externalFunctions: ['add'] })
const sum = m.run({
externalFunctions: {
add: (a: number, b: number) => a + b,
},
})
When external functions are declared, the interpreter uses run_with_external_functions instead of the direct MontyRun::run path.
Asynchronous External Functions
For I/O-bound operations, use runMontyAsync to handle Promise-based external functions:
import { Monty, runMontyAsync } from '@pydantic/monty'
const code = `
async def fetch_data(url):
return await external_fetch(url)
result = await fetch_data("https://example.com")
`
const m = new Monty(code, {
inputs: ['url'],
externalFunctions: ['external_fetch'],
})
const out = await runMontyAsync(m, {
inputs: { url: 'https://example.com' },
externalFunctions: {
external_fetch: async (url: string) => {
const resp = await fetch(url)
return resp.text()
},
},
})
The runMontyAsync helper in crates/monty-js/src/lib.rs internally uses Monty::start and repeatedly awaits Promises that resolve when snapshots are ready.
Manual Snapshot Handling
For fine-grained control over external function calls, manually manage the execution loop:
import { Monty, MontySnapshot, MontyComplete } from '@pydantic/monty'
const m = new Monty('a() + b()', {
externalFunctions: ['a', 'b'],
})
let prog = m.start()
while (prog instanceof MontySnapshot) {
console.log('Calling', prog.functionName, 'with args', await prog.args())
prog = prog.resume({ returnValue: 10 })
}
if (prog instanceof MontyComplete) {
console.log('Final result:', await prog.output) // → 20
}
The MontySnapshot stores the function name, positional/keyword arguments as MontyObject values, and an optional print-callback reference. Calling resume with a returnValue or exception continues execution.
Serialization and Caching
Avoid reparsing overhead by serializing compiled Monty instances:
import { Monty } from '@pydantic/monty'
const m = new Monty('heavy_computation()')
const data = m.dump() // Returns Buffer
// Later – restore without reparsing
const restored = Monty.load(data)
const out = restored.run()
Snapshots can also be serialized using snapshot.dump() and later restored via MontySnapshot.load(buf), enabling suspension and resumption of execution across process boundaries.
Enforcing Resource Limits
Prevent runaway execution by setting resource constraints:
import { Monty, JsResourceLimits } from '@pydantic/monty'
const limits: JsResourceLimits = {
maxAllocations: 10_000,
maxDurationSecs: 2,
maxMemory: 2 * 1024 * 1024, // 2 MiB
maxRecursionDepth: 200,
}
const m = new Monty('while True: pass')
try {
m.run({ limits })
} catch (e) {
console.error('Execution stopped:', e.message)
}
The JsResourceLimits struct in crates/monty-js/src/limits.rs converts these constraints into the internal Rust resource tracker, triggering a MontyRuntimeError when limits are exceeded.
Key Source Files in the Repository
Understanding the source structure helps when debugging or extending the bindings:
| Path | Purpose |
|---|---|
crates/monty-js/README.md |
High-level usage guide and npm-install instructions. |
crates/monty-js/src/lib.rs |
Exposes the public N-API symbols (Monty, runMontyAsync, error classes, etc.). |
crates/monty-js/src/monty_cls.rs |
Core implementation of the JavaScript-side Monty class, MontySnapshot, MontyComplete, and all option structs. |
crates/monty-js/src/convert.rs |
Handles conversion between MontyObject and napi values (Maps ↔ Map, Sets ↔ Set, Bytes ↔ Buffer, BigInt handling, marker objects). |
crates/monty-js/src/exceptions.rs |
Definitions of JsMontyException and the three typed error classes that are re-exported. |
crates/monty-js/src/limits.rs |
Definition of JsResourceLimits and conversion to the internal Rust tracker. |
crates/monty-js/package.json |
NPM package metadata (entry point, TypeScript typings). |
crates/monty-js/index.d.ts |
TypeScript declaration file that describes the public API for IDEs and tsc. |
These files together form the napi-rs bridge that lets you write ordinary JavaScript/TypeScript code while safely executing untrusted Python inside the Rust sandbox.
Summary
- Install the bridge via
npm install @pydantic/montyto use Monty from JavaScript/TypeScript with napi-rs. - Instantiate the
Montyclass with Python code and optional metadata (inputs, external functions, resource limits). - Execute using
run()for synchronous code,runMontyAsync()for async external functions, or manualstart()/resume()for fine-grained control over external calls. - Convert data automatically between JavaScript and Python via the layer in
crates/monty-js/src/convert.rs, supportingMap,Set,Buffer, andBigInt. - Serialize compiled instances with
dump()andload()to avoid reparsing overhead, or serialize snapshots to pause and resume execution later. - Enforce sandbox constraints using
JsResourceLimitsto cap memory, CPU time, allocations, and recursion depth.
Frequently Asked Questions
How do I install the Monty npm package?
Install the package using your preferred package manager. The package includes prebuilt binaries for common platforms, so you typically do not need a Rust toolchain:
npm install @pydantic/monty
Once installed, import the Monty class and any helper functions you need from the package.
Can I use async/await with Monty external functions?
Yes, but you must use the runMontyAsync helper instead of the standard run() method. The run() method cannot suspend for asynchronous work, so runMontyAsync (exported from crates/monty-js/src/lib.rs) manages the event loop for you. It internally calls Monty::start, awaits the Promise returned by your async external function, and resumes execution with the resolved value.
What happens when Python code hits a resource limit?
When execution exceeds any limit defined in JsResourceLimits (such as maxMemory, maxDurationSecs, or maxAllocations), the interpreter stops and throws a MontyRuntimeError. This error class (defined in crates/monty-js/src/exceptions.rs) wraps the internal Rust error and includes details about which limit was breached, allowing you to catch and handle sandbox violations gracefully.
How do I serialize and restore a Monty instance?
To avoid the overhead of reparsing Python source code, use the dump() instance method to serialize a compiled Monty instance into a Buffer. Later, use the static Monty.load(buffer) method to restore the instance without re-parsing. This is particularly useful for caching compiled code in serverless environments or saving state between process restarts. You can also serialize individual snapshots using snapshot.dump() and restore them with MontySnapshot.load(buf) to pause and resume execution across different sessions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →