Really Adequate Go-python Engine
RAGE is an embeddable Python 3.14 runtime written in Go. It allows you to run Python code directly from your Go applications without any external dependencies or CGO.
RAGE comes with optional batteries included. If you wan't some of the python standard library there are many modules you can use. If you don't want to use the python standard library at all you don't have to. If you only want to use a few modules from the standard library you can pick and choose which modules are made available to scripts.
Check out the demo to see RAGE in action.
- Pure Go implementation - no CGO, no external Python installation required
- Embeddable - designed to be used as a library in Go applications
- ClassBuilder API - define full-featured Python classes in Go with operators, properties, methods, and protocols
- Timeout support - prevent infinite loops with execution timeouts
- Context cancellation - integrate with Go's context for graceful shutdown
- Resource limits - recursion depth, memory usage, and collection size caps for safe sandboxed execution
- Standard library modules - math, random, string, sys, time, re, collections, json, os, datetime, typing, asyncio, csv, itertools, functools, io, base64, abc, dataclasses, copy, operator
- Go interoperability - call Go functions from Python, define Python classes in Go, exchange values bidirectionally
go get github.com/ATSOTECK/rage- Unit tests:
go test ./...- In-package runtime tests (
internal/runtime/*_test.go) β operations, conversions, items/slicing, type primitives - External compile+execute tests (
test/*_test.go) β builtins, stdlib modules - Compiler tests (
internal/compiler/*_test.go)
- In-package runtime tests (
- Integration tests: 136 scripts with 3322 tests covering data types, operators, control flow, functions, classes, exceptions, exception groups, exception chaining, exception hierarchy, generators, comprehensions, closures, decorators, imports, context managers, metaclasses, descriptors, string formatting, dataclasses, copy module, super(), type unions, and more
- Run with
go run test/integration/integration_test_runner.go
- Run with
package main
import (
"fmt"
"log"
"github.com/ATSOTECK/rage/pkg/rage"
)
func main() {
// Simple one-liner
result, err := rage.Run(`print("Hello from Python!")`)
if err != nil {
log.Fatal(err)
}
// Evaluate an expression
result, err = rage.Eval(`2 ** 10`)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 1024
}package main
import (
"fmt"
"log"
"github.com/ATSOTECK/rage/pkg/rage"
)
func main() {
// Create a new execution state
state := rage.NewState()
defer state.Close()
// Set variables accessible from Python
state.SetGlobal("name", rage.String("World"))
state.SetGlobal("count", rage.Int(42))
// Run Python code
_, err := state.Run(`
greeting = "Hello, " + name + "!"
doubled = count * 2
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
`)
if err != nil {
log.Fatal(err)
}
// Get variables set by Python
fmt.Println(state.GetGlobal("greeting")) // Hello, World!
fmt.Println(state.GetGlobal("doubled")) // 84
fmt.Println(state.GetGlobal("total")) // 15
}package main
import (
"fmt"
"strings"
"github.com/ATSOTECK/rage/pkg/rage"
)
func main() {
state := rage.NewState()
defer state.Close()
// Register a Go function callable from Python
state.Register("shout", func(s *rage.State, args ...rage.Value) rage.Value {
if len(args) == 0 {
return rage.None
}
text, _ := rage.AsString(args[0])
return rage.String(strings.ToUpper(text) + "!")
})
// Call it from Python
state.Run(`message = shout("hello world")`)
fmt.Println(state.GetGlobal("message")) // HELLO WORLD!
}The ClassBuilder API lets you define Python classes entirely in Go β with operators, properties, methods, context managers, and more. The resulting classes are used from Python like any native class.
package main
import (
"fmt"
"math"
"github.com/ATSOTECK/rage/pkg/rage"
)
func main() {
state := rage.NewState()
defer state.Close()
// Define a Vec2 class with operators, methods, and properties
vec2 := rage.NewClass("Vec2").
Init(func(s *rage.State, self rage.Object, args ...rage.Value) error {
self.Set("x", args[0])
self.Set("y", args[1])
return nil
}).
Str(func(s *rage.State, self rage.Object) (string, error) {
x, _ := rage.AsFloat(self.Get("x"))
y, _ := rage.AsFloat(self.Get("y"))
return fmt.Sprintf("Vec2(%g, %g)", x, y), nil
}).
Add(func(s *rage.State, self rage.Object, other rage.Value) (rage.Value, error) {
o, _ := rage.AsObject(other)
x1, _ := rage.AsFloat(self.Get("x"))
y1, _ := rage.AsFloat(self.Get("y"))
x2, _ := rage.AsFloat(o.Get("x"))
y2, _ := rage.AsFloat(o.Get("y"))
result := self.Class().NewInstance()
result.Set("x", rage.Float(x1+x2))
result.Set("y", rage.Float(y1+y2))
return result, nil
}).
Property("length", func(s *rage.State, self rage.Object) (rage.Value, error) {
x, _ := rage.AsFloat(self.Get("x"))
y, _ := rage.AsFloat(self.Get("y"))
return rage.Float(math.Sqrt(x*x + y*y)), nil
}).
Method("distance_to", func(s *rage.State, self rage.Object, args ...rage.Value) (rage.Value, error) {
o, _ := rage.AsObject(args[0])
x1, _ := rage.AsFloat(self.Get("x"))
y1, _ := rage.AsFloat(self.Get("y"))
x2, _ := rage.AsFloat(o.Get("x"))
y2, _ := rage.AsFloat(o.Get("y"))
dx, dy := x2-x1, y2-y1
return rage.Float(math.Sqrt(dx*dx + dy*dy)), nil
}).
Build(state)
state.SetGlobal("Vec2", vec2)
state.Run(`
a = Vec2(3, 4)
b = Vec2(6, 8)
print(a + b) # Vec2(9, 12)
print(a.length) # 5.0
print(a.distance_to(b)) # 5.0
`)
}The ClassBuilder supports 60+ methods covering the full Python data model:
| Category | ClassBuilder Methods |
|---|---|
| Initialization | Init, InitKw, New, NewKw |
| Operators | Add, Sub, Mul, TrueDiv, FloorDiv, Mod, Pow, MatMul, LShift, RShift, And, Or, Xor (+ reflected R* and in-place I* variants) |
| Unary | Neg, Pos, Abs, Invert |
| Comparison | Eq, Ne, Lt, Le, Gt, Ge, Hash |
| Container | Len, GetItem, SetItem, DelItem, Contains, Missing, Bool, Dir |
| Iteration | Iter, Next, Reversed, Await, AIter, ANext |
| String | Str, Repr, Format |
| Numeric | IntConv, FloatConv, ComplexConv, BytesConv, Index, Round |
| Attributes | GetAttribute, GetAttr, SetAttr, DelAttr |
| Descriptors | DescGet, DescSet, DescDelete, SetName |
| Context manager | Enter, Exit |
| Callable | Call, CallKw |
| Methods | Method, MethodKw, StaticMethod, StaticMethodKw, ClassMethod, ClassMethodKw, Property, PropertyWithSetter |
| Class-level | Attr, ClassGetItem, InitSubclass, Dunder, Base, Bases |
See the demo for a complete example with four Go-defined classes (Vec2, Color, Inventory, GameSession).
package main
import (
"context"
"fmt"
"time"
"github.com/ATSOTECK/rage/pkg/rage"
)
func main() {
// Using timeout
_, err := rage.RunWithTimeout(`
while True:
pass # infinite loop
`, 2*time.Second)
fmt.Println(err) // execution timeout
// Using context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
state := rage.NewState()
defer state.Close()
_, err = state.RunWithContext(ctx, `
x = 0
while True:
x += 1
`)
fmt.Println(err) // context deadline exceeded
}RAGE supports three configurable resource limits for safe sandboxed execution. All default to 0 (unlimited).
package main
import (
"fmt"
"github.com/ATSOTECK/rage/pkg/rage"
)
func main() {
// Set limits at creation time
state := rage.NewStateWithModules(
rage.WithAllModules(),
rage.WithMaxRecursionDepth(100), // Max call stack depth
rage.WithMaxMemoryBytes(50*1024*1024), // ~50MB memory cap
rage.WithMaxCollectionSize(100000), // Max elements per list/dict/set
)
defer state.Close()
// Or set them after creation
state.SetMaxRecursionDepth(200)
state.SetMaxMemoryBytes(100 * 1024 * 1024)
state.SetMaxCollectionSize(500000)
// Recursion limit raises RecursionError (catchable in Python)
_, err := state.Run(`
def infinite():
return infinite()
try:
infinite()
except RecursionError:
print("Caught recursion limit!")
`)
// Memory limit raises MemoryError
state2 := rage.NewStateWithModules(
rage.WithMaxMemoryBytes(1024),
)
defer state2.Close()
_, err = state2.Run(`s = "x" * 1000000`)
fmt.Println(err) // MemoryError: memory limit exceeded
// Collection size limit raises MemoryError
state3 := rage.NewStateWithModules(
rage.WithMaxCollectionSize(100),
)
defer state3.Close()
_, err = state3.Run(`lst = [i for i in range(200)]`)
fmt.Println(err) // MemoryError: list size limit exceeded
// Check tracked memory usage
fmt.Println(state.AllocatedBytes())
}| Limit | Error Raised | What It Protects Against |
|---|---|---|
MaxRecursionDepth |
RecursionError |
Infinite/excessive recursion |
MaxMemoryBytes |
MemoryError |
Runaway frame/string allocations |
MaxCollectionSize |
MemoryError |
Unbounded list/dict/set growth |
Collection size limits are enforced across all growth paths: append, extend, insert, update, add, setdefault, comprehensions, [0] * N repetition, concatenation, and constructors (list(), dict(), set(), etc.).
By default, NewState() enables all available stdlib modules. For more control:
// Create state with only specific modules
state := rage.NewStateWithModules(
rage.WithModule(rage.ModuleMath),
rage.WithModule(rage.ModuleString),
)
defer state.Close()
// Or enable multiple at once
state := rage.NewStateWithModules(
rage.WithModules(rage.ModuleMath, rage.ModuleString, rage.ModuleTime),
)
// Create a bare state with no modules
state := rage.NewBareState()
state.EnableModule(rage.ModuleMath) // Enable one
state.EnableAllModules() // Or enable all later
// Check what's enabled
if state.IsModuleEnabled(rage.ModuleMath) {
fmt.Println("Math module is available")
}RAGE uses the rage.Value interface to represent Python values.
// Primitives
none := rage.None
b := rage.Bool(true)
i := rage.Int(42)
f := rage.Float(3.14)
c := rage.Complex(1, 2) // 1+2j
s := rage.String("hello")
// Collections
list := rage.List(rage.Int(1), rage.Int(2), rage.Int(3))
tuple := rage.Tuple(rage.String("a"), rage.String("b"))
dict := rage.Dict("name", rage.String("Alice"), "age", rage.Int(30))
// From Go values (automatic conversion)
val := rage.FromGo(map[string]any{
"numbers": []any{1, 2, 3},
"active": true,
})if rage.IsInt(val) {
n, _ := rage.AsInt(val)
fmt.Println("Integer:", n)
}
if rage.IsList(val) {
items, _ := rage.AsList(val)
for _, item := range items {
fmt.Println(item)
}
}
if rage.IsDict(val) {
dict, _ := rage.AsDict(val)
for k, v := range dict {
fmt.Printf("%s: %v\n", k, v)
}
}val := state.GetGlobal("result")
// Get the underlying Go value
goVal := val.GoValue()
// Or use type-specific helpers
if n, ok := rage.AsInt(val); ok {
fmt.Println("Got integer:", n)
}For repeated execution, compile once and run multiple times:
state := rage.NewState()
defer state.Close()
// Compile once
code, err := state.Compile(`result = x * 2`, "multiply.py")
if err != nil {
log.Fatal(err)
}
// Execute multiple times with different inputs
for i := 0; i < 5; i++ {
state.SetGlobal("x", rage.Int(int64(i)))
state.Execute(code)
fmt.Println(state.GetGlobal("result"))
}
// Output: 0, 2, 4, 6, 8// Compilation errors
_, err := rage.Run(`def broken syntax`)
if compErr, ok := err.(*rage.CompileErrors); ok {
for _, e := range compErr.Errors {
fmt.Println("Compile error:", e)
}
}
// Runtime errors
_, err = rage.Run(`x = 1 / 0`)
if err != nil {
fmt.Println("Runtime error:", err)
}RAGE is under active development. Currently supported:
Most of the language has been implemented.
| Module | Constant | Description |
|---|---|---|
| math | rage.ModuleMath |
Mathematical functions (sin, cos, sqrt, etc.) |
| random | rage.ModuleRandom |
Random number generation |
| string | rage.ModuleString |
String constants (ascii_letters, digits, etc.) |
| sys | rage.ModuleSys |
System information (version, platform) |
| time | rage.ModuleTime |
Time functions (time, sleep) |
| re | rage.ModuleRe |
Regular expressions |
| collections | rage.ModuleCollections |
Container datatypes (Counter, defaultdict) |
| json | rage.ModuleJSON |
JSON encoding and decoding |
| os | rage.ModuleOS |
OS interface (environ, path manipulation) |
| datetime | rage.ModuleDatetime |
Date and time types |
| typing | rage.ModuleTyping |
Type hint support |
| asyncio | rage.ModuleAsyncio |
Basic async/await support |
| csv | rage.ModuleCSV |
CSV file reading and writing |
| itertools | rage.ModuleItertools |
Iterator building blocks (chain, combinations, permutations, etc.) |
| functools | rage.ModuleFunctools |
Higher-order functions (partial, reduce, lru_cache, wraps) |
| io | rage.ModuleIO |
File I/O operations |
| base64 | rage.ModuleBase64 |
Base16, Base32, Base64 data encodings |
| abc | rage.ModuleAbc |
Abstract base classes (ABC, ABCMeta, abstractmethod) |
| dataclasses | rage.ModuleDataclasses |
Data class decorator and field utilities |
| copy | rage.ModuleCopy |
Shallow and deep copy operations |
| operator | rage.ModuleOperator |
Operator functions (length_hint, index) |
| enum | rage.ModuleEnum |
Enumerations (Enum, IntEnum, StrEnum, Flag, IntFlag, auto, unique) |
- Full async/await - async generators, async context managers (basic support via asyncio module)
Reflection builtins (globals, locals, compile, exec, eval) are opt-in and disabled by default. Enable them explicitly if needed:
state := rage.NewStateWithModules(
rage.WithAllModules(),
rage.WithBuiltin(rage.BuiltinGlobals),
rage.WithBuiltin(rage.BuiltinExec),
)For running untrusted code, combine resource limits with timeouts for defense in depth:
state := rage.NewStateWithModules(
rage.WithAllModules(),
rage.WithMaxRecursionDepth(100),
rage.WithMaxMemoryBytes(50 * 1024 * 1024),
rage.WithMaxCollectionSize(100000),
)
defer state.Close()
_, err := state.RunWithTimeout(untrustedCode, 5*time.Second)Each State is NOT safe for concurrent use. Create separate States for concurrent execution, or use appropriate synchronization.
// Safe: separate states per goroutine
for i := 0; i < 10; i++ {
go func(n int) {
state := rage.NewState()
defer state.Close()
state.SetGlobal("n", rage.Int(int64(n)))
state.Run(`result = n * n`)
}(i)
}MIT
pls