monty-go: Pure-Go wrapper for Pydantic's Monty Python Interpreter

Aug 30, 2026 07:02 PM - 4 hours ago 4

Run LLM-generated Python safely from Go — nary containers, nary CGO, nary subprocess.

A pure-Go wrapper astir Pydantic's Monty Python interpreter, compiled to WebAssembly and loaded via wazero. Your Go supplier writes Python code, monty-go executes it successful a sandboxed WASM lawsuit pinch sub-millisecond startup, and pauses whenever the codification calls an outer usability truthful your Go codification tin grip it.

go get github.com/fugue-labs/monty-go

LLMs activity faster, cheaper, and much reliably erstwhile they constitute codification alternatively of making sequential instrumentality calls. Instead of:

Agent → tool_call("search", {query: "weather london"}) → result Agent → tool_call("search", {query: "weather tokyo"}) → result Agent → tool_call("compare", {a: result1, b: result2}) → result

The LLM writes:

london = search(query="weather london") tokyo = search(query="weather tokyo") compare(a=london, b=tokyo)

One exemplary telephone alternatively of three. The Python codification calls your Go functions, Monty pauses astatine each call, your Go codification executes it, and Monty resumes. No containers. No sandbox services. No exec(). Just a 2.9MB WASM binary embedded successful your Go binary.

For motivation, see:

  • Programmatic Tool Calling from Anthropic
  • Code Execution pinch MCP from Anthropic
  • Code Mode from Cloudflare
  • Smol Agents from Hugging Face
package main import ( "context" "fmt" "log" montygo "github.com/fugue-labs/monty-go" ) func main() { runner, err := montygo.New() if err != nil { log.Fatal(err) } defer runner.Close() result, err := runner.Execute(context.Background(), "x * 2 + y", map[string]any{"x": 10, "y": 5}, ) if err != nil { log.Fatal(err) } fmt.Println(result) // 25 }

External Functions (Pause/Resume)

The existent powerfulness is outer usability calls. Monty pauses execution whenever Python codification calls a usability you've declared, your Go callback handles it, and Monty resumes pinch the return value:

result, err := runner.Execute(ctx, ` london = get_weather("London") tokyo = get_weather("Tokyo") f"{london['city']}: {london['temp']}°C, {tokyo['city']}: {tokyo['temp']}°C" `, nil, montygo.WithExternalFunc(func(ctx context.Context, call *montygo.FunctionCall) (any, error) { city, _ := call.Args["city"].(string) // Your existent implementation present — HTTP call, database query, anything. return map[string]any{"city": city, "temp": 22}, nil }, montygo.Func("get_weather", "city")), ) // result: "London: 22°C, Tokyo: 22°C"

Multiple functions activity the aforesaid measurement — registry them each and dispatch by name:

result, err := runner.Execute(ctx, code, nil, montygo.WithExternalFunc(func(ctx context.Context, call *montygo.FunctionCall) (any, error) { switch call.Name { case "search": return doSearch(call.Args) case "calculate": return doCalculate(call.Args) case "store": return doStore(call.Args) default: return nil, fmt.Errorf("unknown function: %s", call.Name) } }, montygo.Func("search", "query"), montygo.Func("calculate", "expression"), montygo.Func("store", "key", "value"), ), )

Prevent runaway codification pinch memory, time, allocation, and recursion limits:

result, err := runner.Execute(ctx, code, inputs, montygo.WithLimits(montygo.Limits{ MaxDuration: 5 * time.Second, MaxMemoryBytes: 10 * 1024 * 1024, // 10 MB MaxAllocations: 100000, MaxRecursionDepth: 100, }), )

Infinite loops, representation bombs, and heavy recursion each terminate cleanly pinch a *MontyError. Go's context.Context deadlines are besides respected — cancel the discourse and the WASM lawsuit stops.

Capture Python print() output:

var output strings.Builder _, err := runner.Execute(ctx, `print("step 1 done")`, nil, montygo.WithPrintFunc(func(s string) { output.WriteString(s) }), ) fmt.Print(output.String()) // "step 1 done\n"

Python filesystem and situation entree routes done your Go callback:

result, err := runner.Execute(ctx, ` from pathlib import Path data = Path("/config/settings.json").read_text() data `, nil, montygo.WithOsCallFunc(func(ctx context.Context, call *montygo.OsCall) (any, error) { switch call.Function { case "Path.read_text": path, _ := call.Args[0].(string) return readFromYourStorage(path) case "Path.exists": path, _ := call.Args[0].(string) return existsInYourStorage(path), nil default: return nil, fmt.Errorf("blocked: %s", call.Function) } }), )

No filesystem entree happens unless your callback allows it.

monty-go is designed to powerfulness code-mode successful Gollem, the accumulation supplier model for Go. Instead of sequential instrumentality calls, the LLM writes Python that calls your devices arsenic functions — Monty executes it safely, and Gollem orchestrates the full thing.

Here's what this looks for illustration pinch Gollem:

import ( "github.com/fugue-labs/gollem" "github.com/fugue-labs/gollem/provider/anthropic" montygo "github.com/fugue-labs/monty-go" ) // Your existing Gollem devices — search, calculate, store, whatever. searchTool := gollem.FuncTool[SearchParams]("search", "Search the knowledge base", doSearch) calcTool := gollem.FuncTool[CalcParams]("calculate", "Run calculations", doCalc) // Create a code-mode instrumentality that wraps your toolset pinch Monty. // The LLM writes Python code, Monty executes it, outer usability calls // way to your Go tools. codeMode := NewCodeModeTool(runner, searchTool, calcTool) agent := gollem.NewAgent[Analysis](anthropic.New(), gollem.WithTools[Analysis](codeMode), gollem.WithSystemPrompt[Analysis](`You person a codification execution tool. Write Python codification to telephone the disposable functions. Available functions: - search(query: str) -> dict: Search the knowledge base - calculate(expression: str) -> float: Evaluate mathematics expressions Write codification that calls these functions and returns the result.`), ) result, _ := agent.Run(ctx, "Compare Q3 and Q4 gross and cipher the maturation rate")

With 1 exemplary call, the LLM writes:

q3 = search(query="Q3 revenue") q4 = search(query="Q4 revenue") growth = calculate(expression=f"({q4['revenue']} - {q3['revenue']}) / {q3['revenue']} * 100") {"q3": q3, "q4": q4, "growth_rate": growth}

Monty pauses 3 times (two searches, 1 calculation), your Go functions grip each one, and the last consequence flows backmost done Gollem's typed output pipeline. Three instrumentality calls successful 1 LLM round-trip.

Why Gollem + monty-go:

Traditional instrumentality calling Code-mode pinch monty-go
LLM calls One per instrumentality use One for each tools
Latency N × exemplary round-trip 1 × exemplary round-trip + μs execution
Cost N × input/output tokens 1 × input/output tokens
Logic LLM reasons measurement by step LLM writes the logic once
Control flow None (sequential only) Loops, conditionals, variables
Error handling LLM must respond to each failure try/except successful Python
Security ✅ (tools are Go functions) ✅ (WASM sandbox + your callbacks)

Gollem gives you compile-time type safety, system output, guardrails, costs tracking, middleware, and multi-provider support. monty-go gives you unafraid embedded Python execution. Together, your agents do much activity per exemplary call.

github.com/fugue-labs/gollem — The accumulation supplier model for Go.

┌─────────────────────────────────────────────────────────┐ │ Your Go Application │ │ │ │ runner, _ := montygo.New() │ │ result, _ := runner.Execute(ctx, code, inputs, opts) │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────┐ │ │ │ wazero (pure Go WASM runtime) │ │ │ │ │ │ │ │ ┌───────────────────────────┐ │ │ │ │ │ monty.wasm (2.9 MB) │ │ ◄── go:embed │ │ │ │ Monty Python Interpreter │ │ │ │ │ │ compiled to wasm32-wasi │ │ │ │ │ └──────────┬────────────────┘ │ │ │ │ │ │ │ │ │ region connected outer telephone │ │ │ │ │ │ │ │ └─────────────┼───────────────────┘ │ │ │ │ │ ▼ │ │ ExternalFunc callback ──► your Go codification ──► resume │ │ OsCallFunc callback ──► your Go codification ──► resume │ │ PrintFunc callback ──► your Go codification │ └─────────────────────────────────────────────────────────┘
  • No CGO. wazero is simply a pure-Go WebAssembly runtime.
  • No subprocess. The WASM binary is embedded via go:embed and compiled erstwhile astatine startup.
  • Fresh lawsuit per call. Each Execute() gets an isolated WASM instance. No authorities leaks betwixt calls.
  • JSON astatine the boundary. All information crossing the Go↔WASM bound is JSON. Go types representation naturally: int→float64, string→string, bool→bool, nil→None, []any→list, map[string]any→dict.
// Create a reusable runner. Compiles the WASM module once. runner, err := montygo.New() defer runner.Close() // Execute Python codification pinch inputs and options. result, err := runner.Execute(ctx, code, inputs, opts...) // Options: montygo.WithExternalFunc(fn, // registry callable functions montygo.Func("search", "query", "limit"), // pinch named parameters montygo.Func("calculate", "expression"), ) montygo.WithOsCallFunc(fn) // grip filesystem/env access montygo.WithLimits(montygo.Limits{...}) // assets limits montygo.WithPrintFunc(fn) // seizure people output // FunctionCall provides named args (positional mapped by param name): call.Args["query"].(string) // entree by parameter name call.ArgsJSON() // pre-serialized JSON string
Python Go (result) Go (input)
int float64 int, float64
float float64 float64
str string string
bool bool bool
None nil nil
list, tuple []any []any
dict map[string]any map[string]any
set []any

Python exceptions go *montygo.MontyError:

result, err := runner.Execute(ctx, "1 / 0", nil) var me *montygo.MontyError if errors.As(err, &me) { fmt.Println(me.Message) // "Traceback... ZeroDivisionError: section by zero" }

Tracks upstream Monty v0.0.11.

  • Arithmetic, drawstring operations, f-strings, slicing
  • Functions, lambdas, closures, generators
  • for/while loops, if/elif/else, break/continue
  • try/except/finally/else, raise, objection hierarchy
  • List/dict/set comprehensions, dict/set position operators
  • range, len, sum, min, max, sorted, reversed, enumerate, zip, map, filter, all, any, getattr
  • isinstance, type, int(), float(), str(), bool(), abs()
  • print() pinch sep and extremity kwargs
  • PEP 448 generalized unpacking (*args, **kwargs successful calls, literals, etc.)
  • Nested and augmented subscript duty (a[i][j] = v, a[i] += 1)
  • Tuple comparison (<, >, <=, >=)
  • Multi-module imports (import a, b, c)
  • Stdlib modules: mathematics (all functions), re, datetime, json, and sys/typing/asyncio subsets
  • import os, from pathlib import Path (routed done OsCallFunc)
  • Dataclass instances travel done outer usability calls (args, returns, and method calls aboveground pinch method_call=true)
  • Resource limits: time, memory, allocations, recursion depth
  • Class definitions (only dataclass instances via outer I/O; upstream Monty flags people def arsenic "coming soon")
  • match statements (coming soon upstream)
  • Context managers (with ...)
  • Rest of stdlib and each third-party libraries
  • float('inf') / float('nan') (JSON serialization limitation successful this bridge)

97 end-to-end tests covering each testable script from Monty's halfway trial suite:

Covers: basal expressions, people variants, each objection types, information type round-tripping, outer functions (args, kwargs, mixed, analyzable types, chaining, loops), input handling and scoping, assets limits (timeout, recursion, memory, allocations), OS calls, builtins, power flow, lambdas/closures, and execution isolation.

Requires Rust pinch wasm32-wasip1 target and Go 1.23+:

rustup target adhd wasm32-wasip1 make build # compiles Rust → WASM, copies to monty.wasm make test # builds and runs Go tests

monty-go exists because of Monty, created by Samuel Colvin and the Pydantic team. Monty is simply a genuinely caller portion of engineering — a minimal, unafraid Python expert written from scratch successful Rust, purpose-built for AI agents. The penetration that LLMs should constitute codification alternatively of making sequential instrumentality calls, and that you request a safe expert (not a container) to execute it, is what makes code-mode possible.

Samuel and the Pydantic squad person a way grounds of building foundational devices that the full ecosystem builds connected — Pydantic, Pydantic AI, Logfire, and now Monty. This task is simply a Go span to their work, and we're grateful they built it.

MIT

More