Elixir wrapper for Monty, a minimal secure Python interpreter written in Rust.
ExMonty provides safe execution of Python code from Elixir with:
- Microsecond startup — no Python runtime needed
- Interactive execution — code pauses at external function calls, hands control to Elixir, and resumes with results
- Resource limits — control memory, CPU time, and recursion depth
- Full type mapping — Python types map naturally to Elixir types
Quick Start
# Simple evaluation
{:ok, result, output} = ExMonty.eval("2 + 2")
# result = 4, output = ""
# With inputs
{:ok, runner} = ExMonty.compile("result = x + y", inputs: ["x", "y"])
{:ok, result, output} = ExMonty.run(runner, %{"x" => 10, "y" => 20})
# result = 30Interactive Execution
{:ok, runner} = ExMonty.compile("result = fetch(url)", inputs: ["url"])
{:ok, progress} = ExMonty.start(runner, %{"url" => "https://example.com"})
case progress do
{:name_lookup, name, snapshot, _output} ->
# Provide a function object for the undefined name
{:ok, next} = ExMonty.resume(snapshot, {:ok, {:function, name}})
{:function_call, call, snapshot, _output} ->
response = do_fetch(call.name, call.args)
{:ok, next} = ExMonty.resume(snapshot, {:ok, response})
{:method_call, call, snapshot, _output} ->
# Dataclass method call — call.args[0] is the instance
result = handle_method(call.name, call.args, call.kwargs)
{:ok, next} = ExMonty.resume(snapshot, result)
{:complete, value, _output} ->
value
endSee ExMonty.Sandbox for a high-level handler that automates the interactive loop.
Summary
Functions
Compiles Python code into a reusable runner.
Serializes a runner to a binary for storage or transfer.
Serializes a future snapshot to a binary.
Serializes a snapshot to a binary for storage or transfer.
Compiles and runs Python code in one call.
Deserializes a future snapshot from a binary.
Deserializes a runner from a binary.
Deserializes a snapshot from a binary.
Returns the list of pending call IDs from a future snapshot.
Resumes interactive execution from a snapshot with a result value.
Resumes interactive execution from a future snapshot with results for pending calls.
Mount-aware variant of resume_futures/2. After resolving futures,
any subsequent OS calls are intercepted by the mount table — closes
the gap where futures resumption would otherwise bypass mount routing.
Mount-aware variant of resume/2. Pass :no_handler as the result
for an :os_call snapshot to delegate to upstream's
OsFunction::on_no_handler semantics (PermissionError for FS,
RuntimeError for non-FS).
Runs a compiled runner to completion with the given inputs.
Starts interactive execution of a compiled runner.
Mount-aware variant of start/3. Filesystem operations matching a mount
in the leased mount table are intercepted in Rust without surfacing to
Elixir. Unmounted FS ops and non-FS ops surface as :os_call progress
for fallback dispatch.
Types
@type error_reason() :: term()
@type future_snapshot() :: reference()
@type limits() :: %{ optional(:max_allocations) => non_neg_integer(), optional(:max_duration_secs) => float(), optional(:max_memory) => non_neg_integer(), optional(:gc_interval) => non_neg_integer(), optional(:max_recursion_depth) => non_neg_integer() }
@type progress() :: {:function_call, ExMonty.FunctionCall.t(), snapshot(), String.t()} | {:method_call, ExMonty.FunctionCall.t(), snapshot(), String.t()} | {:os_call, ExMonty.OsCall.t(), snapshot(), String.t()} | {:name_lookup, String.t(), snapshot(), String.t()} | {:resolve_futures, future_snapshot(), String.t()} | {:complete, term(), String.t()}
@type runner() :: reference()
@type snapshot() :: reference()
Functions
@spec compile( String.t(), keyword() ) :: {:ok, runner()} | {:error, error_reason()}
Compiles Python code into a reusable runner.
The runner can be executed multiple times with different inputs via run/3 or start/3.
External function names no longer need to be declared upfront — they are
auto-detected at runtime via name lookup. When the code references an undefined
name, execution pauses with a {:name_lookup, name, snapshot, output} progress
tuple, allowing the host to provide a value or function.
Options
:inputs- list of input variable names (default:[]):script_name- name for the script in tracebacks (default:"main.py")
Examples
{:ok, runner} = ExMonty.compile("result = x * 2", inputs: ["x"])
{:ok, runner} = ExMonty.compile("result = fetch(url)", inputs: ["url"])
Serializes a runner to a binary for storage or transfer.
Examples
{:ok, runner} = ExMonty.compile("result = x + 1", inputs: ["x"])
{:ok, binary} = ExMonty.dump(runner)
{:ok, restored} = ExMonty.load_runner(binary)
@spec dump_future_snapshot(future_snapshot()) :: {:ok, binary()} | {:error, term()}
Serializes a future snapshot to a binary.
Note: This consumes the future snapshot.
Serializes a snapshot to a binary for storage or transfer.
Note: This consumes the snapshot — it cannot be used for resumption after dumping.
@spec eval( String.t(), keyword() ) :: {:ok, term(), String.t()} | {:error, error_reason()}
Compiles and runs Python code in one call.
Convenience function that combines compile/2 and run/3.
Options
:inputs- map of input variable names to values (default:%{}):limits- resource limits map (default:nil):script_name- script name for tracebacks (default:"main.py")
Examples
{:ok, 4, ""} = ExMonty.eval("result = 2 + 2")
{:ok, 30, ""} = ExMonty.eval("result = x + y",
inputs: %{"x" => 10, "y" => 20}
)
@spec load_future_snapshot(binary()) :: {:ok, future_snapshot()} | {:error, term()}
Deserializes a future snapshot from a binary.
Deserializes a runner from a binary.
Examples
{:ok, runner} = ExMonty.load_runner(binary)
Deserializes a snapshot from a binary.
@spec pending_call_ids(future_snapshot()) :: [non_neg_integer()]
Returns the list of pending call IDs from a future snapshot.
Examples
ids = ExMonty.pending_call_ids(futures)
# [1, 2, 3]
@spec resume(snapshot(), {:ok, term()} | {:error, atom(), String.t()} | :undefined) :: {:ok, progress()} | {:error, error_reason()}
Resumes interactive execution from a snapshot with a result value.
For :function_call, :method_call, and :os_call snapshots, the result
should be {:ok, value} for successful returns or {:error, type, message} for errors.
For :name_lookup snapshots, the result should be:
{:ok, {:function, name}}— provide a callable function object{:ok, value}— provide any value for the name:undefined— raiseNameErrorin Python
Examples
# Function call result
{:ok, next_progress} = ExMonty.resume(snapshot, {:ok, "response body"})
{:ok, next_progress} = ExMonty.resume(snapshot, {:error, :runtime_error, "fetch failed"})
# Name lookup result
{:ok, next_progress} = ExMonty.resume(snapshot, {:ok, {:function, "my_func"}})
{:ok, next_progress} = ExMonty.resume(snapshot, :undefined)
@spec resume_futures(future_snapshot(), [{non_neg_integer(), term()}]) :: {:ok, progress()} | {:error, error_reason()}
Resumes interactive execution from a future snapshot with results for pending calls.
Each result is a {call_id, {:ok, value}} or {call_id, {:error, type, message}} tuple.
Examples
ids = ExMonty.pending_call_ids(futures)
results = Enum.map(ids, fn id -> {id, {:ok, compute(id)}} end)
{:ok, next_progress} = ExMonty.resume_futures(futures, results)
@spec resume_futures_with_mounts( future_snapshot(), [{non_neg_integer(), term()}], ExMonty.Mount.Lease.t() ) :: {:ok, progress()} | {:error, error_reason()}
Mount-aware variant of resume_futures/2. After resolving futures,
any subsequent OS calls are intercepted by the mount table — closes
the gap where futures resumption would otherwise bypass mount routing.
@spec resume_with_mounts(snapshot(), term(), ExMonty.Mount.Lease.t()) :: {:ok, progress()} | {:error, error_reason()}
Mount-aware variant of resume/2. Pass :no_handler as the result
for an :os_call snapshot to delegate to upstream's
OsFunction::on_no_handler semantics (PermissionError for FS,
RuntimeError for non-FS).
Runs a compiled runner to completion with the given inputs.
Returns the result value and any captured print output.
Options
:limits- resource limits map (default:nilfor default limits)
Examples
{:ok, runner} = ExMonty.compile("result = x + y", inputs: ["x", "y"])
{:ok, result, output} = ExMonty.run(runner, %{"x" => 1, "y" => 2})
# result = 3, output = ""
@spec start(runner(), map(), keyword()) :: {:ok, progress()} | {:error, error_reason()}
Starts interactive execution of a compiled runner.
Returns a progress tuple that indicates the current state of execution. Use pattern matching to handle function calls, OS calls, futures, or completion.
Options
:limits- resource limits map (default:nil)
Progress Values
{:name_lookup, name, snapshot, output}— paused at unresolved name lookup. Resume with{:ok, {:function, name}}to provide a callable,{:ok, value}for a constant, or:undefinedto raiseNameError.{:function_call, %ExMonty.FunctionCall{}, snapshot, output}— paused at external function call{:method_call, %ExMonty.FunctionCall{}, snapshot, output}— paused at dataclass method call (first arg is the dataclass instance){:os_call, %ExMonty.OsCall{}, snapshot, output}— paused at OS/filesystem operation{:resolve_futures, future_snapshot, output}— paused waiting for async futures{:complete, value, output}— execution finished
Examples
{:ok, runner} = ExMonty.compile("result = fetch(url)", inputs: ["url"])
{:ok, {:name_lookup, "fetch", snapshot, _output}} =
ExMonty.start(runner, %{"url" => "https://example.com"})
# Provide the function, then handle the actual call
{:ok, {:function_call, call, snapshot2, _}} =
ExMonty.resume(snapshot, {:ok, {:function, "fetch"}})
@spec start_with_mounts(runner(), ExMonty.Mount.Lease.t(), map(), keyword()) :: {:ok, progress()} | {:error, error_reason()}
Mount-aware variant of start/3. Filesystem operations matching a mount
in the leased mount table are intercepted in Rust without surfacing to
Elixir. Unmounted FS ops and non-FS ops surface as :os_call progress
for fallback dispatch.
Most users invoke this transparently via ExMonty.Sandbox.run/2 with
the :mounts option.