A Go library for embedding and running Python code directly in your Go applications with high performance and concurrency support.
- Embed Python code directly in Go using
go:embed - Type-safe API with Go generics for input/output
- Concurrent execution with a pool of sub-interpreters
- Automatic Python library discovery on macOS, Linux, and other Unix systems
- Multiple execution modes: return values, write to streams, or use custom I/O
go get github.com/adamkeys/serpent- Go 1.18 or later
- Python 3 shared library installed on your system
package main
import (
"fmt"
"log"
"github.com/adamkeys/serpent"
)
func main() {
lib, err := serpent.Lib()
if err != nil {
log.Fatal(err)
}
if err := serpent.Init(lib); err != nil {
log.Fatal(err)
}
program := serpent.Program[int, int]("def run(input): return input * 2")
result, err := serpent.Run(program, 21)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // Output: 42
}See the examples/ directory for more complete demonstrations including concurrent execution, embedding Python files, and using external libraries like HuggingFace Transformers.
Lib() (string, error)- Automatically discovers the Python shared library pathInit(libPath string) error- Initializes the Python interpreter with a worker poolInitSingleWorker(libPath string) error- Initializes with a single worker (for libraries that don't support sub-interpreters)Close() error- Cleans up and shuts down the interpreter
Run[I, O](program Program[I, O], input I) (O, error)- Executes Python code and returns the resultRunWrite[I](w io.Writer, program Program[I, Writer], input I) error- Executes Python code that writes to a Go io.Writer
For programs you want to call multiple times, use Load to create a reusable executable:
Load[I, O](program Program[I, O]) (*Executable[I, O], error)- Loads a program for repeated executionLoadWriter[I](program Program[I, Writer]) (*WriterExecutable[I], error)- Loads a writer program for repeated execution
exec, err := serpent.Load(program)
if err != nil {
log.Fatal(err)
}
defer exec.Close()
// Call multiple times - state persists between calls
result1, _ := exec.Run(input1)
result2, _ := exec.Run(input2)An Executable is pinned to a single worker on its first Run() call, so module-level state (imports, global variables) persists across calls. This is useful for expensive initialization like loading ML models:
from transformers import pipeline
# Loaded once, reused across all Run() calls
ner = pipeline("ner")
def run(input):
return ner(input)A Program[I, O] is simply a string containing Python code:
type Program[I, O any] stringYour Python code should define a run function:
def run(input):- ForRun, return the result valuedef run(input, writer):- ForRunWrite, write to the provided writer
Define a run function that takes input and returns the result:
def run(input):
return input.upper()When using RunWrite, your run function receives a writer object:
def run(input, writer):
writer.write(f"Hello from Python: {input}\n")The writer object provides:
write(data)- Write string or bytes (strings are auto-encoded as UTF-8)flush()- Flush the output (no-op, writes are unbuffered)
The writer is automatically closed when your function returns.
Python code can import any library available in the Python environment:
from transformers import pipeline
ner = pipeline("ner", grouped_entities=True)
def run(input):
entities = ner(input)
return [e["word"] for e in entities]Note: Libraries that don't support sub-interpreters require initialization with InitSingleWorker() instead of Init().
The examples/ directory contains several demonstrations:
- hello/ - Basic concurrent "Hello World" example
- identity/ - Simple identity transformation
- transformers/ - Named entity recognition using HuggingFace Transformers
LIBPYTHON_PATH- Override automatic library discovery by specifying the Python shared library path directly
Serpent uses purego to dynamically load and call Python's C API without CGO. It manages a pool of Python sub-interpreters (each running on its own OS thread) to enable safe concurrent execution of Python code from multiple goroutines.
Input and output values are serialized as JSON, providing a simple and type-safe interface between Go and Python.
- ✅ macOS (Darwin)
- ✅ Linux
- ✅ Unix-like systems