Ruby bindings for Monty, a minimal and secure Python interpreter written in Rust by Pydantic. Built with Magnus.
Monty is a Python interpreter designed specifically for AI agents. Unlike CPython, it prioritizes security and embeddability over compatibility with the full Python ecosystem.
Key properties of the Monty interpreter:
- Sub-microsecond startup — no VM boot, no module loading, ready instantly
- Strict sandboxing — no filesystem, no network, no environment variable access by default
- CPython-comparable performance — not a toy interpreter
- External function mediation — all I/O is controlled by the host application
- Snapshot/resume — serialize execution state mid-flight and restore it later
- Resource limits — cap memory, allocations, execution time, and recursion depth
Monty supports a practical Python subset: functions, closures, async/await, type hints, dataclasses, list/dict/set comprehensions, and exceptions. It does not support arbitrary classes (coming soon), match statements, or the Python standard library beyond sys, typing, asyncio, dataclasses, and json.
If you're building AI agents, tool-use pipelines, or code evaluation features in Ruby, Monty lets you safely execute LLM-generated Python without:
- Containers or VMs — no Docker, no subprocess spawning, no cold starts
- Security risks — the interpreter can't touch the filesystem, network, or env vars unless you explicitly allow it via external functions
- Language mismatch — your orchestration stays in Ruby while computation happens in Python
- AI agent tool execution — LLMs generate Python code; your Ruby app runs it safely and returns the result
- Computed fields and expressions — let users write Python expressions that your app evaluates (e.g. spreadsheet formulas, data transformations, scoring functions)
- Sandboxed scripting — embed a user-facing scripting language in your Ruby application with deterministic resource limits
- Polyglot data pipelines — bridge Python's data manipulation idioms (list comprehensions, dict operations) into a Ruby-native workflow
- Code evaluation APIs — build "run this code" endpoints without worrying about arbitrary code execution
[!NOTE] Experimental gem
This gem was created as an experiment to see how Opus 4.6 and ChatGPT Codex 5.3 would be able to take the Monty crate from the Pydantic team and bind it to Ruby. All of the code in this gem has been generated by AI Agents.
┌─────────────────────────────────────────────────────┐
│ Ruby application │
│ │
│ Monty::Run.new(python_code, inputs: ["x"]) │
│ run.call(42) # => result │
│ │
├─────────────────────────────────────────────────────┤
│ Ruby wrappers (lib/monty/) │
│ - Keyword arguments, blocks, idiomatic Ruby API │
│ - call_with_externals { |fn| ... } │
│ │
├─────────────────────────────────────────────────────┤
│ Magnus FFI layer (ext/monty/src/) │
│ - #[magnus::wrap] structs → Ruby classes │
│ - MontyObject ↔ Ruby value conversion │
│ - Error mapping: MontyException → Monty::Error │
│ │
├─────────────────────────────────────────────────────┤
│ Monty interpreter (Rust crate) │
│ - MontyRun: parse once, execute many times │
│ - Sandboxed VM with resource tracking │
│ - External function call / snapshot / resume │
└─────────────────────────────────────────────────────┘
The native extension is compiled as a C-compatible dynamic library (cdylib) via rb-sys and Magnus. No tokio runtime is needed — Monty's execution is synchronous from Rust's perspective (Python async/await is handled internally by the interpreter's cooperative state machine).
Value conversion happens at the boundary: Ruby objects are converted to MontyObject variants on the way in, and converted back to Ruby objects on the way out. This means there's no shared mutable state between Ruby and Python — each call is a clean value-in, value-out exchange.
External functions allow Python code to call back into Ruby. When the interpreter hits an external function call, it pauses execution and returns a Monty::FunctionCall to the host. The host resolves the call in Ruby and resumes the interpreter with the result. This is how you give sandboxed Python controlled access to I/O, databases, APIs, or anything else.
Add to your Gemfile:
gem "monty-rb"Or install directly:
gem install monty-rbRequires Rust 1.90+ for compilation.
require "monty"
# Evaluate a Python expression
run = Monty::Run.new("x + y", inputs: ["x", "y"])
run.call(1, 2) # => 3
# Python functions
code = <<~PYTHON
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
factorial(n)
PYTHON
run = Monty::Run.new(code, inputs: ["n"])
run.call(10) # => 3628800
# Reusable — parse once, call many times
run = Monty::Run.new("x * 2", inputs: ["x"])
run.call(5) # => 10
run.call(21) # => 42Ruby values are automatically converted to Python and back:
| Ruby | Python | Ruby |
|---|---|---|
nil |
None |
nil |
true / false |
True / False |
true / false |
Integer |
int |
Integer |
Float |
float |
Float |
String |
str |
String |
Array |
list |
Array |
Hash |
dict |
Hash |
Symbol |
str |
String |
Tuples are returned as frozen Arrays. Nested structures are converted recursively.
run = Monty::Run.new("print('hello')\n42")
result = run.call(capture_output: true)
result[:result] # => 42
result[:output] # => "hello\n"Prevent runaway code from consuming unbounded resources:
run = Monty::Run.new("x ** x ** x", inputs: ["x"])
run.call(100, limits: {
max_duration: 2.0, # seconds
max_memory: 10_485_760, # bytes
max_allocations: 100_000,
max_recursion_depth: 500
})
# Raises Monty::ResourceError if any limit is exceededMonty scripts can call external functions that you implement in Ruby. This is the primary mechanism for giving sandboxed Python controlled access to external resources:
code = <<~PYTHON
response = fetch("https://api.example.com/data")
response.upper()
PYTHON
run = Monty::Run.new(code, external_functions: ["fetch"])
# Block-based API (recommended)
result = run.call_with_externals do |call|
case call.function_name
when "fetch"
Net::HTTP.get(URI(call.args[0]))
end
end
# Capture output and apply limits
result = run.call_with_externals(capture_output: true, limits: { max_duration: 2.0 }) do |call|
case call.function_name
when "fetch"
Net::HTTP.get(URI(call.args[0]))
end
end
# result[:result] => Python return value
# result[:output] => captured stdout
# Manual step-through API
run = Monty::Run.new(code, external_functions: ["fetch"])
progress = run.start
while progress.is_a?(Monty::FunctionCall)
result = handle_function(progress.function_name, progress.args)
progress = progress.resume(result)
end
final_value = progress.valueMonty::Run instances can be serialized for caching or storage. Since parsing is separated from execution, you can parse once and reuse across requests:
run = Monty::Run.new("x * 2", inputs: ["x"])
bytes = run.dump
# Later, or in another process...
restored = Monty::Run.load(bytes)
restored.call(21) # => 42# Monty::Error - base error class (< StandardError)
# Monty::SyntaxError - Python syntax errors
# Monty::ResourceError - resource limit exceeded
# Monty::ConsumedError - using a consumed Run/FunctionCall
begin
run = Monty::Run.new("1 / 0")
run.call
rescue Monty::Error => e
puts e.message
endbundle install
bundle exec rake compile # build native extension
bundle exec rake spec # run tests
bundle exec rake # compile + test- Monty — the Rust interpreter this gem wraps
- Magnus — Rust ↔ Ruby bindings framework
- rb-sys — Ruby native extension build system for Rust
- Pydantic — the team behind Monty
MIT