rage

module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 21, 2026 License: MIT

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)
  • 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

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL