cd /news/ai-tools/monty-go-pure-go-wrapper-for-pydanti… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-115709] src=github.com β†— pub= topic=ai-tools verified=true sentiment=↑ positive

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

Fugue Labs released monty-go, a pure-Go wrapper for Pydantic's Monty Python interpreter that runs LLM-generated Python code safely in a WebAssembly sandbox via wazero, with sub-millisecond startup and no containers, CGO, or subprocesses. The library, available via `go get github.com/fugue-labs/monty-go`, embeds a 2.9MB WASM binary and supports external function calls, allowing Go agents to handle pauses when Python code invokes declared functions, plus configurable limits for duration, memory, allocations, and recursion depth.

read9 min views19 publishedAug 30, 2026
monty-go: Pure-Go wrapper for Pydantic's Monty Python Interpreter
Image: Michielbdejong (auto-discovered)

Run LLM-generated Python safely from Go β€” no containers, no CGO, no subprocess.

A pure-Go wrapper around Pydantic's Monty Python interpreter, compiled to WebAssembly and loaded via wazero. Your Go agent writes Python code, monty-go executes it in a sandboxed WASM instance with sub-millisecond startup, and s whenever the code calls an external function so your Go code can handle it.

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

LLMs work faster, cheaper, and more reliably when they write code instead of making sequential tool 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 model call instead of three. The Python code calls your Go functions, Monty s at each call, your Go code executes it, and Monty resumes. No containers. No sandbox services. No exec()

. Just a 2.9MB WASM binary embedded in your Go binary.

For motivation, see:

Programmatic Tool Callingfrom AnthropicCode Execution with MCPfrom AnthropicCode Modefrom CloudflareSmol Agentsfrom 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
}

The real power is external function calls. Monty s execution whenever Python code calls a function you've declared, your Go callback handles it, and Monty resumes with 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 real implementation here β€” 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 work the same way β€” register them all 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 code with 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, memory bombs, and deep recursion all terminate cleanly with a *MontyError

. Go's context.Context

deadlines are also respected β€” cancel the context and the WASM instance 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 environment access routes through 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 access happens unless your callback allows it.

monty-go is designed to power code-mode in Gollem, the production agent framework for Go. Instead of sequential tool calls, the LLM writes Python that calls your tools as functions β€” Monty executes it safely, and Gollem orchestrates the whole thing.

Here's what this looks like with Gollem:

import (
    "github.com/fugue-labs/gollem"
    "github.com/fugue-labs/gollem/provider/anthropic"
    montygo "github.com/fugue-labs/monty-go"
)

// Your existing Gollem tools β€” 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 tool that wraps your toolset with Monty.
// The LLM writes Python code, Monty executes it, external function calls
// route to your Go tools.
codeMode := NewCodeModeTool(runner, searchTool, calcTool)

agent := gollem.NewAgent[Analysis](anthropic.New(),
    gollem.WithTools[Analysis](codeMode),
    gollem.WithSystemPrompt[Analysis](`You have a code execution tool.
Write Python code to call the available functions. Available functions:
- search(query: str) -> dict: Search the knowledge base
- calculate(expression: str) -> float: Evaluate math expressions
Write code that calls these functions and returns the result.`),
)

result, _ := agent.Run(ctx, "Compare Q3 and Q4 revenue and calculate the growth rate")

With one model 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 s three times (two searches, one calculation), your Go functions handle each one, and the final result flows back through Gollem's typed output pipeline. Three tool calls in one LLM round-trip.

Why Gollem + monty-go:

Traditional tool calling Code-mode with monty-go
LLM calls
One per tool use One for all tools
Latency
N Γ— model round-trip 1 Γ— model round-trip + ΞΌs execution
Cost
N Γ— input/output tokens 1 Γ— input/output tokens
Logic
LLM reasons step by step LLM writes the logic once
Control flow
None (sequential only) Loops, conditionals, variables
Error handling
LLM must react to each failure try/except in Python
Security
βœ… (tools are Go functions) βœ… (WASM sandbox + your callbacks)

Gollem gives you compile-time type safety, structured output, guardrails, cost tracking, middleware, and multi-provider support. monty-go gives you secure embedded Python execution. Together, your agents do more work per model call.

** github.com/fugue-labs/gollem** β€” The production agent framework 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  β”‚  β”‚                    β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚                    β”‚
β”‚  β”‚             β”‚                   β”‚                    β”‚
β”‚  β”‚      on external call      β”‚                    β”‚
β”‚  β”‚             β”‚                   β”‚                    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚
β”‚                β”‚                                        β”‚
β”‚                β–Ό                                        β”‚
β”‚  ExternalFunc callback ──► your Go code ──► resume      β”‚
β”‚  OsCallFunc callback   ──► your Go code ──► resume      β”‚
β”‚  PrintFunc callback    ──► your Go code                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

No CGO. wazero is a pure-Go WebAssembly runtime.No subprocess. The WASM binary is embedded viago:embed

and compiled once at startup.Fresh instance per call. EachExecute()

gets an isolated WASM instance. No state leaks between calls.JSON at the boundary. All data crossing the Go↔WASM boundary is JSON. Go types map 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 code with inputs and options.
result, err := runner.Execute(ctx, code, inputs, opts...)

// Options:
montygo.WithExternalFunc(fn,                     // register callable functions
    montygo.Func("search", "query", "limit"),    // with named parameters
    montygo.Func("calculate", "expression"),
)
montygo.WithOsCallFunc(fn)                       // handle filesystem/env access
montygo.WithLimits(montygo.Limits{...})          // resource limits
montygo.WithPrintFunc(fn)                        // capture print output

// FunctionCall provides named args (positional mapped by param name):
call.Args["query"].(string)    // access 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 become *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: division by zero"
}

Tracks upstream Monty v0.0.11.

  • Arithmetic, string operations, f-strings, slicing
  • Functions, lambdas, closures, generators for

/while

loops,if

/elif

/else

,break

/continue

try

/except

/finally

/else

,raise

, exception hierarchy- List/dict/set comprehensions, dict/set view operators range

,len

,sum

,min

,max

,sorted

,reversed

,enumerate

,zip

,map

,filter

,all

,any

,getattr

isinstance

,type

,int()

,float()

,str()

,bool()

,abs()

print()

withsep

andend

kwargs- PEP 448 generalized unpacking ( *args

,**kwargs

in calls, literals, etc.) - Nested and augmented subscript assignment ( a[i][j] = v

,a[i] += 1

) - Tuple comparison ( <

,>

,<=

,>=

) - Multi-module imports ( import a, b, c

) - Stdlib modules: math

(all functions),re

,datetime

,json

, andsys

/typing

/asyncio

subsets import os

,from pathlib import Path

(routed through OsCallFunc)- Dataclass instancesflow through external function calls (args, returns, and method calls surface withmethod_call=true

) - Resource limits: time, memory, allocations, recursion depth

  • Class definitions (only dataclass instances via external I/O; upstream Monty flags class def

as "coming soon") match

statements (coming soon upstream)- Context managers ( with ...

) - Rest of stdlib and all third-party libraries float('inf')

/float('nan')

(JSON serialization limitation in this bridge)

97 end-to-end tests covering every testable scenario from Monty's core test suite:

make test

Covers: basic expressions, print variants, all exception types, data type round-tripping, external functions (args, kwargs, mixed, complex types, chaining, loops), input handling and scoping, resource limits (timeout, recursion, memory, allocations), OS calls, builtins, control flow, lambdas/closures, and execution isolation.

Requires Rust with wasm32-wasip1

target and Go 1.23+:

rustup target add 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 a genuinely novel piece of engineering β€” a minimal, secure Python interpreter written from scratch in Rust, purpose-built for AI agents. The insight that LLMs should write code instead of making sequential tool calls, and that you need a safe interpreter (not a container) to execute it, is what makes code-mode possible.

Samuel and the Pydantic team have a track record of building foundational tools that the whole ecosystem builds on β€” Pydantic, Pydantic AI, Logfire, and now Monty. This project is a Go bridge to their work, and we're grateful they built it.

MIT

── more in #ai-tools 4 stories Β· sorted by recency
── more on @fugue labs 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/monty-go-pure-go-wra…] indexed:0 read:9min 2026-08-30 Β· β€”