Cobra

A Python-like scripting language, implemented in Go. Faster than CPython on every benchmark, shipped as one static binary, with async/await, structs, modules and a batteries-included standard library.

Download v0.2.0 Quick start See examples
two verified-identical engines tree-walker + bytecode VM ~7 MB static binary REPL ยท LSP ยท VS Code

Sixty seconds of Cobra

Python's feel, without significant whitespace: blocks open with a keyword and close with end. Indentation is purely cosmetic.

# fib.cobra
def fib(n)
    if n < 2
        return n
    end
    return fib(n - 1) + fib(n - 2)
end

for i in range(8)
    print(fib(i))
end
$ cobra fib.cobra          # tree-walking engine
$ cobra --vm fib.cobra     # bytecode VM (the fast one)
$ cobra                    # REPL, multi-line aware
$ cobra build fib.cobra    # compile to fib.cobrac
$ cobra fib.cobrac         # run the compiled bytecode
0
1
1
2
3
5
8
13

Why Cobra

Built layer by layer โ€” lexer, Pratt parser, tree-walking evaluator, then a bytecode compiler and stack VM โ€” with one rule throughout: every feature lands in both engines, verified to behave identically, tested at every layer.

Outruns CPython

The VM beats CPython 3.14 on all six benchmarks โ€” register-cached dispatch, zero-allocation calls, small-int interning, superinstructions, and a custom dict with cached string hashes.

One static binary

No interpreter install, no venv, no dependency hell. Copy a ~7 MB binary anywhere and run .cobra source or compiled .cobrac bytecode.

async / await

Python's GIL concurrency model with none of the event-loop ceremony: every blocking stdlib call releases the lock, so I/O-bound tasks genuinely overlap. await works at the top level.

Errors before runtime

Undefined variables, assignment to builtins and misplaced break are compile-time errors. Runtime errors carry line numbers โ€” identical messages from both engines.

Batteries included

Nine native modules: fs, math, os, json, time, re, http (client and server), proc, net. All failures catchable.

Real tooling

Multi-line REPLs on both engines, an LSP server (cobra lsp) with live diagnostics from the real parser and compiler, and a VS Code extension with highlighting and IntelliSense.

Benchmarks vs Python

Six equivalent workload pairs, timed against CPython 3.14 on an Apple M-series laptop (wall clock, best of 3). The harness refuses to report numbers unless all three engines print byte-identical output.

benchmarkworkloadCobra VMpython3ratio
fibfib(32), recursive calls0.15s0.15s0.97x
loop10M-iteration while + arithmetic0.37s0.53s0.7x
listsbuild + iterate 2M elements0.13s0.17s0.8x
dicts1M inserts + lookups0.18s0.29s0.6x
strings200k concat/join/split0.04s0.04s0.8x
structs1M objects + method calls0.20s0.21s0.96x
total1.06s1.39s0.8x

Honest caveats: these are microbenchmarks; Python + NumPy wins anything numeric, and PyPI's ecosystem is thirty years deep. Cobra's lane is fast hermetic scripts, embedded scripting in Go applications, and being small enough to fully understand.

The command line

One binary, every mode. Compiled .cobrac files are detected by content, so they run with the plain form too.

commandwhat it does
cobra app.cobrarun a program on the tree-walking engine
cobra --vm app.cobrarun on the bytecode VM โ€” the fast engine
cobra / cobra --vminteractive REPL (multi-line aware, state persists) on either engine
cobra build app.cobra [out.cobrac]compile ahead-of-time to a bytecode file
cobra app.cobracrun compiled bytecode โ€” no parsing, no compiling
cobra app.cobra arg1 arg2extra arguments reach the program via os.args()
cobra --ast app.cobraprint the parsed AST instead of running
cobra --tokens app.cobraprint the raw token stream from the lexer
cobra lspspeak the Language Server Protocol over stdio (any LSP editor)
cobra --versionprint the stamped release version

Build workflow

$ cobra build todo.cobra              # compile next to the source
wrote todo.cobrac
$ cobra build todo.cobra dist/todo    # or pick the output path
wrote dist/todo
$ cobra todo.cobrac :9000 100         # run it โ€” args flow through to os.args()
$ cobra --ast todo.cobrac
cobra: todo.cobrac is a compiled file; --ast needs source

Building Cobra itself

$ make test       # go vet + the full ~500-test suite
$ make build      # optimized binary for this machine -> dist/cobra
$ make install    # build and place cobra on your PATH
$ make release    # test, then cross-compile: macOS arm64/amd64,
                  # linux amd64/arm64, windows amd64 (~7 MB each, static)
$ make bench      # the benchmark suite against python3

The language by example

Every snippet below is parsed by Cobra's real parser as part of this page's build check.

Variables, control flow, loops

let declares, plain = reassigns. Truthiness, operator precedence and and/or returning the deciding operand all follow Python.

let name = "cobra"
let level = 3

if level > 5 and name != "viper"
    print("expert")
elif level > 1
    print("getting there")
else
    print("new")
end

let total = 0
for i in range(1, 11)
    if i % 2 == 0
        continue
    end
    total = total + i
end
print(total)                     # 25 โ€” odd numbers 1..9

while total > 0
    total = total - 10
end

Functions and closures

First-class functions with real closures โ€” inner functions capture and mutate enclosing variables.

def make_counter()
    let count = 0
    def inc()
        count = count + 1
        return count
    end
    return inc
end

let tick = make_counter()
tick()
tick()
print(tick())                    # 3

Lists and dicts

Reference types with deep equality, negative indexing, insertion-ordered dicts, and Python-named methods.

let scores = [70, 95, 80]
scores.sort()
scores.push(60)
print(scores[-1], scores.contains(95))     # 60 true

let ages = {"ada": 36, "alan": 41}
ages["grace"] = 85
print(ages.get("linus", 0))                # 0 โ€” no KeyError surprises
for pair in ages.items()
    print(pair[0], pair[1])
end

Strings

let log = "2026-06-12 ERROR disk full"
print(log.split(" ")[1])                   # ERROR
print(log.upper().startswith("2026"))      # true
print(" | ".join(["a", "b", "c"]))         # a | b | c
print("  padded  ".strip().replace("a", "@"))

Structs โ€” simple classes

init is the constructor, self is implicit, fields are open. Instances print themselves and type() returns the struct name.

struct Point
    def init(x, y)
        self.x = x
        self.y = y
    end
    def length()
        return self.x * self.x + self.y * self.y
    end
    def scale(f)
        self.x = self.x * f
        self.y = self.y * f
    end
end

let p = Point(3, 4)
p.scale(2)
print(p)                         # Point{x: 6, y: 8}
print(p.length(), type(p))       # 100 Point

Errors: try / catch / finally / throw

Throw any value; runtime errors are caught as their message string. Errors unwind through calls to the nearest handler; finally always runs.

def divide(a, b)
    if b == 0
        throw {"code": 400, "reason": "division by zero"}
    end
    return a / b
end

try
    divide(1, 0)
catch err
    print(err["code"], err["reason"])
finally
    print("cleanup always runs")
end

Modules

A module is just a file, run once and cached; its top-level names become members. Native modules (fs, json, โ€ฆ) resolve the same way.

import "geometry.cobra"
import "geometry.cobra" as geo   # cached: same module object

print(geometry.circle_area(2))
print(geo.Rect(3, 4).area())

File I/O and JSON

import "fs"
import "json"

let config = json.parse(fs.read("config.json"))
config["retries"] = 3
fs.write("config.json", json.stringify(config, 2))

for line in fs.read_lines("server.log")
    if line.contains("ERROR")
        print(line)
    end
end

HTTP client and regex

import "http"
import "json"
import "re"

let resp = http.get("https://api.github.com/repos/golang/go")
if resp["status"] == 200
    let repo = json.parse(resp["body"])
    print(repo["stargazers_count"])
end

let emails = re.find_all("\w+@\w+\.\w+", fs.read("contacts.txt"))
print(re.replace("(\w+)-(\w+)", "left-right", "$2-$1"))

An HTTP server in 20 lines

Handlers are plain Cobra functions. A raised error becomes a 500 and the server keeps running.

import "http"
import "json"

let todos = []

def handler(req)
    if req["path"] == "/api/todos" and req["method"] == "POST"
        todos.push(json.parse(req["body"]))
        return {"status": 201, "body": "created"}
    end
    if req["path"] == "/api/todos"
        return {"status": 200,
                "body": json.stringify(todos, 2),
                "headers": {"Content-Type": "application/json"}}
    end
    return {"status": 404, "body": "not found"}
end

http.serve(":8080", handler)

async / await

Calling an async def returns a task immediately. The GIL releases during every blocking call, so these three fetches take as long as the slowest one, not the sum.

import "http"

async def fetch(url)
    return http.get(url)["status"]
end

let tasks = [
    fetch("https://example.com"),
    fetch("https://example.org"),
    fetch("https://example.net")
]
for status in await tasks
    print(status)
end

try
    await fetch("not a url")
catch err
    print("caught:", err)        # task errors surface at the await
end

Processes and sockets

import "proc"
import "net"

let r = proc.shell("git log --oneline | head -3")
print(r["stdout"], r["code"])

let conn = net.connect("example.com:80")
net.send(conn, "HEAD / HTTP/1.0\r\n\r\n")
print(net.recv_line(conn))
net.close(conn)

Under the hood

Two engines, one semantics

A tree-walking evaluator is the readable reference; the bytecode VM is the performance engine. They share one implementation of every value operation, and the test suite requires identical output and identical error messages from both.

A VM tuned like the big ones

Register-cached dispatch loop, frames in a preallocated value array (zero allocation per call), CPython-style small-int interning, an OpLocalConstBin superinstruction, and an open-addressing dict with hashes cached on string objects.

Honest engineering

~500 tests across 12 Go packages: exact-message error tests, cross-engine equivalence on every example file, live-socket server tests, race-detector runs on the async paths, and benchmarks gated on output equality with Python itself.

Download

Cobra v0.2.0 โ€” one static binary per platform. No installer, no runtime, no dependencies: download, chmod +x, run.

platformfilesizemd5
macOS (Apple Silicon) cobra-0.2.0-darwin-arm64 6.7 MB9feaeaaa8db656ea810873020f1b8016
macOS (Intel) cobra-0.2.0-darwin-amd64 7.0 MB61d595cd66cfe1989c5ddad2a25321d5
Linux (x86-64) cobra-0.2.0-linux-amd64 6.8 MB1aaf2996660cc063f9365d46c19dd5bb
Linux (arm64) cobra-0.2.0-linux-arm64 6.5 MB971abc564efc91c131c9331b1d6a7484
Windows (x86-64) cobra-0.2.0-windows-amd64.exe.zip 2.9 MB4b2a36a501148b0ca884bfd2c7907ca7
VS Code extension cobra-lang-0.1.0.vsix 9.7 KB46a8eceaa8eae1d58ecea36eba851496

Install the extension with code --install-extension cobra-lang-0.1.0.vsix โ€” highlighting and snippets work standalone; diagnostics light up when cobra is on your PATH.

All hashes in one file: MD5SUMS.

$ curl -LO https://cobralang.baltavista.com/releases/cobra-0.2.0-darwin-arm64
$ md5 cobra-0.2.0-darwin-arm64           # macOS โ€” on Linux: md5sum
$ chmod +x cobra-0.2.0-darwin-arm64
$ sudo mv cobra-0.2.0-darwin-arm64 /usr/local/bin/cobra
$ cobra --version
cobra 0.2.0