README
¶
RAGE
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.
Features
- 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
Installation
go get github.com/ATSOTECK/rage
Tests
- 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: 135 scripts with 3306 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(), and more
- Run with
go run test/integration/integration_test_runner.go
- Run with
Quick Start
Run Python Code
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
}
Using State for More Control
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
}
Registering Go Functions
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!
}
Defining Python Classes in Go
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).
Timeouts and Context
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
}
Resource Limits
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.).
Controlling Standard Library Modules
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")
}
Working with Values
RAGE uses the rage.Value interface to represent Python values.
Creating 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,
})
Type Checking
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)
}
}
Converting to Go Values
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)
}
Compile Once, Run Many
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
Error Handling
// 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)
}
Current Status
RAGE is under active development. Currently supported:
Implemented
Most of the language has been implemented.
Available Modules
| 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) |
Not Yet Implemented
- Full async/await - async generators, async context managers (basic support via asyncio module)
Security Notes
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)
Thread Safety
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)
}
License
MIT
Contributing
pls
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
rage
command
|
|
|
Demo: Python-as-Configuration for a game server.
|
Demo: Python-as-Configuration for a game server. |
|
internal
|
|
|
stdlib
Package stdlib provides standard library modules for the RAGE Python runtime.
|
Package stdlib provides standard library modules for the RAGE Python runtime. |
|
pkg
|
|
|
rage
Package rage provides a public API for embedding the RAGE Python runtime in Go applications.
|
Package rage provides a public API for embedding the RAGE Python runtime in Go applications. |
|
test
|
|
|
integration
command
Package main runs all Python test scripts and validates their output.
|
Package main runs all Python test scripts and validates their output. |