rakulang — Raku from Python
A pure-source Python package over librakupp's C ABI. No compiled glue: the
loader is ctypes, values cross through
rakupp.h, and the grammar logic lives in a
small Raku shim (rakulang/grammar_shim.raku) the binding evaluates into its
interpreter at startup.
The package is named for the language, in the Raku community's disambiguated
spelling — raku is an unrelated package on PyPI. The engine underneath is
Raku++: rakupp the binary, librakupp the library. import rakulang as raku if you like the short spelling.
Python is the reference binding; the other four follow it.
1. What you need
-
Python 3.9+. No third-party packages —
ctypesis in the standard library. -
librakupp, unless the wheel from PyPI is what you install: that one carries the library inside it. From a checkout, build it at the repo root:cmake -B build -DCMAKE_BUILD_TYPE=Release -DRAKUPP_BUILD_SHARED=ON cmake --build build -j
A build directory configured without
-DRAKUPP_BUILD_SHARED=ONis static-only and this package cannot use it.
2. Install
pip install rakulang
The wheel carries librakupp inside it, so it needs no rakupp on the
machine; rakulang.interpreter().version says which engine it holds, and the
package version is that engine's. It is built for macOS (universal), Linux
(x86_64 and aarch64) and Windows (x64), and each wheel's own platform tag
names the floor it needs.
From a checkout, pip install -e bindings/python instead, after which plain
import rakulang works. The examples below add the directory to sys.path,
so they run against a fresh checkout with nothing installed.
Finding the library usually needs no configuration: if rakupp is on PATH,
the loader takes librakupp from beside it — an installed layout's sibling
lib/, a Homebrew keg's, or the build directory the binary sits in. A
platform wheel carries its own copy and needs nothing at all.
To override, name one: an explicit path to
rakulang.interpreter("/path/to/librakupp.dylib"), or RAKUPP_LIB (the
file), or RAKUPP_HOME (an install prefix with lib/). A library you name
is used as given. If it cannot be loaded you get that error, not a quiet
fall-back to whichever other library happens to be findable — the usual cause
is an architecture mismatch, and falling back makes the symptom (some other
build's behaviour) point nowhere near the cause. Unset the variable to search
instead.
On ELF platforms the library is loaded RTLD_GLOBAL so Raku extensions
dlopen'ed later can resolve rk_* — a requirement from ABI-PLAN A3, not a
preference.
3. Two minutes
Run both examples from the repo root (.so for .dylib on Linux):
RAKUPP_LIB=$PWD/build/librakupp.dylib python3 bindings/python/examples/calc.py
RAKUPP_LIB=$PWD/build/librakupp.dylib python3 bindings/python/examples/shopping.py
calc (examples/calc.py) prints:
2 + 2 = 4
area(3, 4) = 12
primes below 30: 2 3 5 7 11 13 17 19 23 29
stats: count=8 sum=31 mean=3.88 max=9
greet: Hello, Ada! You are 36.
30! = 265252859812191058636308480000000
died: division by zero
shopping (examples/shopping.py) prints:
3 items
milk x 2
bread x 1
eggs x 12
total, computed in Raku: 15
as plain Python data: {'item': [{'name': 'milk', 'qty': '2'}, {'name': 'bread', 'qty': '1'}, {'name': 'eggs', 'qty': '12'}]}
line 2 column 7 while trying <qty>
If you see both, the binding works.
4. Running Raku
import rakulang
raku = rakulang.interpreter() # the process's interpreter
raku.eval("my $x = 41")
raku.eval("$x + 1") # 42 — eval keeps state, like the REPL
raku.version # '3.14.0'
eval returns the last statement's value, converted to Python data.
Define a sub in Raku, call it from Python
Declare the sub with eval — the declaration stays in the interpreter's
mainline scope — then call it by name. Python arguments bind to the
signature's positional parameters, in order, and the return value comes back
as Python data:
raku.eval("""
sub area($w, $h) { $w * $h }
sub total(@prices) { @prices.sum }
sub describe(%p) { "%p<name> costs %p<price>" }
sub hello($name, $greeting = 'Hello') { "$greeting, $name!" }
""")
raku.call("area", 3, 4) # 12 — one argument per parameter
raku.call("total", [1, 2, 3.5]) # 6.5 — a list binds to @prices
raku.call("describe", {"name": "tea", "price": 3})
# 'tea costs 3' — a dict binds to %p
raku.call("hello", "Ada") # 'Hello, Ada!' — the default fills in
raku.can("area") # True; False before the eval
The subs may as well come from a file: raku.eval(open("calc.raku").read())
declares everything in it, which is how examples/calc.py
loads ../examples/calc.raku.
Arguments convert automatically — None, bool, int (any width), float,
str, list, tuple, dict. Anything else raises TypeError. The
conversion is by Python type, so sub flag(Bool $b) wants True, not 1.
The call is checked exactly as a call written in Raku is. Too few or too many
arguments, or a value that fails a type constraint, raise RakuError
carrying the engine's message instead of binding silently:
raku.call("area", 3)
# RakuError: Calling area(Int) will never work with declared signature ($w, $h)
Named parameters. call passes positionals only. A sub declared with
named parameters is called by writing the call in Raku and evaluating it:
raku.eval('sub greet(:$name, :$age = 0) { "Hello, $name! You are $age." }')
raku.eval('greet(name => "Ada", age => 36)') # 'Hello, Ada! You are 36.'
raku.call("greet", {"name": "Ada"}) # RakuError — the dict is one
# positional Hash, not two names
Or declare the sub to take a hash, sub greet(%who), and pass a dict — which
is what calc.raku does. multi subs, slurpy *@args, and our subs inside
a package (raku.call("Geo::perimeter", 3, 4)) all resolve through call. A
method is reached through eval: raku.eval("Counter.new.bump.n").
5. Parsing with grammars
log = rakulang.Grammar.from_file("log.raku", name="Log", actions="LogActions")
m = log.parse(text) # a handle, not data; None if no match
for line in m["line"]: # lazy: one engine call per leaf
print(line["ip"].str(), line["status"].int())
print(m["line"][0]["size"].made) # computed by LogActions, in the parse
everything = m.tree() # eager, opt-in (~1.4× the parse)
from_file(path, name=..., actions=...) compiles and caches: identical
source compiles once, and each named compile is isolated in its own wrapper
package, so recompiling an edited grammar under the same name works and
earlier handles keep the body they were compiled from. Without name there
is no wrapper, and a same-name recompile raises the engine's
X::Redeclaration. name may be omitted only when the grammar declaration
is the file's last statement.
parse(text, rule=...) anchors to the whole input and returns a Match or
None; pass rule= to parse a fragment with one rule. Indexing builds a
lazy path — nothing crosses the boundary until a terminal: .str(),
.int(), .num(), .made, bool(), len(), iteration, .tree(), or
.match() (which returns an independent rooted Match).
6. Values
Raku Int → int, Num/Rat → float, Str → str, List → list,
Hash → dict, True/False → bool, Any → None. The same rules run
in reverse for arguments.
An integer wider than 64 bits arrives as an ordinary Python int: the C ABI
hands integers over as an int64, so the binding reads the digits instead
whenever that saturates. In a tree(), a
match node with no sub-captures becomes its matched text — qty is the
string "2" — so use .int() on the node, or an actions class, for numbers.
7. Errors
RakuError is a Raku die crossing the boundary. ParseError is its
subclass for a diagnosed non-match, carrying .line, .column, .rule and
.pos:
try:
g.parse(text, strict=True)
except rakulang.ParseError as e:
print(f"line {e.line} column {e.column} while trying <{e.rule}>")
A failed terminal on a missing capture raises RakuError; bool() and
len() answer False/0 instead, which is how you probe for one.
8. Lifetime and threading
One interpreter per process, created on first use; one host thread talks to
it at a time (Raku code inside it threads freely). Matches hold rooted values
in the interpreter — close() them, use a with block, or let the GC do it.
Values from eval and call are already plain Python data and need nothing.
9. Testing
build/rakupp tools/bindings-smoke.raku
Runs both examples in all five languages and checks the output against
../examples/expected/. For the deep gate — this
binding driving the same grammar and 2000-line corpus as the Raku reference
driver, byte-compared — run build/rakupp tools/grammar-smoke.raku. Both run
in CI on every push.
Troubleshooting
Four questions settle most reports: which Python ran, which copy of the package it imported, which library file that copy loaded, and which engine that library is. One line answers all four:
python3 -c "import rakulang, sys; r = rakulang.interpreter(); print(sys.executable, rakulang.__file__, r._lib._name, r.version, sep='\n')"
_lib._name is the loaded file's path — a private attribute, fine for
diagnosis. Compare the last line with rakupp --version for the binary on
PATH: the library reports the plain release number, the binary adds its git
describe suffix, and the leading numbers should agree.
The search order decides the third line. A library you name is used as
given: the path passed to interpreter(), else RAKUPP_LIB, else
RAKUPP_HOME/lib/. Otherwise the loader takes, in this order, a copy bundled
inside the package (rakulang/_lib/), the library beside the rakupp on
PATH (its sibling lib/, then its own directory), and the system linker
path.
librakupp not found. Nothing bundled, nothing beside rakupp, nothing
on the linker path; the message lists every path it tried. If it continues
A rakupp binary WAS found (…) but its build carries no shared library, the
build directory on PATH is configured without -DRAKUPP_BUILD_SHARED=ON.
Reconfigure it with that option and build again, set RAKUPP_LIB to a build
that has the library, or install the platform wheel (below).
RAKUPP_LIB names …, which could not be loaded. A named library is
authoritative; the loader does not fall back to another. The quoted dlopen
error says why: no such file when the path does not exist — a relative path
is resolved against the current directory, so a shell profile wants an
absolute one — or the architecture mismatch below. Unset the variable to
search instead.
incompatible architecture. Your python3 and the library disagree
(file $(which python3) against file build/librakupp.dylib). Build the
library for your interpreter's architecture:
cmake -B build-x64 -DCMAKE_OSX_ARCHITECTURES=x86_64 -DRAKUPP_BUILD_SHARED=ON ...
.version is older than rakupp --version. The library the loader
found is a leftover. A build directory keeps its librakupp.* files until a
build overwrites them, and a directory reconfigured without
-DRAKUPP_BUILD_SHARED=ON never does: the binary beside them stays current
while the library keeps the version it had. make rakupp rebuilds the binary
only; cmake --build <dir> with no target rebuilds the library too. Delete
the leftovers or rebuild the shared target — the loader cannot tell a leftover
from a fresh build.
AttributeError: dlsym(…, rk_…): symbol not found. Raised from
interpreter() when the library lacks an entry point this package declares,
which means the library predates the package. Rebuild it from the same
checkout the package came from.
import rakulang is not the copy you edited. rakulang.__file__ says
which one loaded. pip install -e bindings/python imports the checkout
itself; a plain pip install bindings/python, or a wheel, copies the package
at install time and does not follow later edits — reinstall to refresh. python
and python3 can be different interpreters with different site-packages.
The platform wheel. tools/build-wheel.sh <build-dir> bundles that
build's library into the package, and a pip install of the result needs no
rakupp on PATH and no variables. The bundled copy is a snapshot: .version
reports it, and refreshing it is a rebuild and a reinstall. The script builds
in a scratch venv, so it needs pip access to PyPI.
tools/build-wheel.sh build dist-wheel
python3 -m pip install --force-reinstall --no-deps dist-wheel/rakulang-*.whl
On Windows run it under Git Bash, and name the configuration directory the
Visual Studio generator writes to, build/Release, where librakupp.dll
is.
rk_new refused: an interpreter is already live in this process.
Something already created an interpreter in this process. Use
rakulang.interpreter(), which returns the shared one.
Numbers (G0 gate, 2026-08-11, M-series macOS)
2000-line / 168 KB access log, seven-token grammar, best of three
(python3 bindings/python/bench.py build-shared):
| phase | rakupp direct | via shim, engine-side | Python host | host/direct |
|---|---|---|---|---|
| parse | 11.6 ms | 10.7 ms | 10.5 ms | 1.0× |
| tree (eager) | 68.0 ms | 68.2 ms | 93.0 ms | 1.4× |
| selective (2 fields × 2000 lines) | 1.7 ms | 25.9 ms | 52.6 ms | ~31× |
Parse is engine-bound — the host boundary adds nothing. Selective access costs ~13 µs per leaf (half the walk sub, half ABI + ctypes); it exists because eager conversion of everything nobody asked for is usually the worse deal, but if a profile ever shows the per-leaf cost dominating a real workload, that is GRAMMAR-PLAN G4's cue (a native Match walker), not a reason to grow this layer.
Metadata
Release files for rakulang 5.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| rakulang-5.2.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| rakulang-5.2.0-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details |
| rakulang-5.2.0-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details |
| rakulang-5.2.0-py3-none-macosx_11_0_universal2.whl | Python 3 | none | macOS 11.0+ universal2 (ARM64, x86-64) | Details |
Total release size: 37.4 MB
Release files / rakulang-5.2.0-py3-none-win_amd64.whl
| Download URL | rakulang-5.2.0-py3-none-win_amd64.whl |
|---|---|
| Size | 6.3 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
2d54ac0d5db517079cb8fa14bfd80f44b35b47fbfeeb404e89eeb325da817161
|
|
BLAKE2b-256 checksum How to use checksums |
ce7ab917aea6cd05fc3d75f42e6c4f6e6c4f4059d37798fa4bd12e5919af35b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / rakulang-5.2.0-py3-none-manylinux_2_28_x86_64.whl
| Download URL | rakulang-5.2.0-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 8.0 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
810236fcc97a15e457d8d51af69860b6f502ea3a23d6f1afa4c4fe0ced2aa28d
|
|
BLAKE2b-256 checksum How to use checksums |
32afce42ae09b919a82d53dbe695dd7b2c1a6297e32cfcf2dda8aa90e108a4da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / rakulang-5.2.0-py3-none-manylinux_2_28_aarch64.whl
| Download URL | rakulang-5.2.0-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 7.9 MB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
803115bb9d907975db11e0c5557a77879dd7ab30158bb1b6922316c78212287b
|
|
BLAKE2b-256 checksum How to use checksums |
e73b2f132a51982b708c062bb36905fb5edaf75dae9793aa233951eee3715157
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / rakulang-5.2.0-py3-none-macosx_11_0_universal2.whl
| Download URL | rakulang-5.2.0-py3-none-macosx_11_0_universal2.whl |
|---|---|
| Size | 15.2 MB |
| Tags | Python 3 macOS 11.0+ universal2 (ARM64, x86-64) |
|
SHA-256 checksum How to use checksums |
6823bebb32698f128e4919ee533988d256ce7cc42bca30f61b3cbe8f3afb86c6
|
|
BLAKE2b-256 checksum How to use checksums |
b445c6a11b7c76728aa38ce8c42630004df800bd1d357c36d0f5935417ac0d1a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log