Bun runs Python through a separately installed CPython host library (bun_python_host, ABI 1) that it loads on demand. JavaScript that never touches Python never loads it. Once the host is configured, bun app.py and bun python run CPython in the Bun process, and bun:python calls the same interpreter from JavaScript. Python packages and projects are managed with the embedded UV (see Buv and PyJS).
export BUN_PYTHON_HOST_LIBRARY=/opt/bun-python/lib/libbun_python_host.so
export BUN_PYTHON_LIBPYTHON=/opt/python/lib/libpython3.13.so
bun app.py --flag # a Python file, arguments passed through
bun python -c "print('hi')" # the python CLI: -c, -m, - (stdin)
Configure the host
Python dispatch is enabled only when BUN_PYTHON_HOST_LIBRARY is set. Without it, .py files go through the normal Bun resolution and JavaScript keeps its usual startup.
| Variable | Purpose |
|---|---|
BUN_PYTHON_HOST_LIBRARY | Path to the bun_python_host shared library. Required. Its bun_py_abi_version() must return 1. |
BUN_PYTHON_LIBPYTHON | Path to the CPython shared library (libpython3.x.so, .dylib or python3x.dll). Required for .py dispatch. |
BUN_PYTHON_EXECUTABLE | Explicit sys.executable. Takes precedence over the two below. |
VIRTUAL_ENV | An activated virtual environment: bin/python (Scripts/python.exe on Windows) becomes sys.executable. |
BUV_RUNTIME | A CPython prefix (bin/python3, or python.exe on Windows). On Linux and macOS it is otherwise derived from the lib/ directory of BUN_PYTHON_LIBPYTHON. |
On Windows, set BUV_RUNTIME or BUN_PYTHON_EXECUTABLE, or activate a virtual environment: the prefix cannot be derived from the DLL path.
Run Python files and the CLI
A path ending in .py or .pyw runs in CPython, with Unicode arguments, sys.argv, the script directory on sys.path, the selected interpreter and the exit status preserved. bun python (or bun python3) accepts CPython's command line.
bun ./script.py 茅t茅馃悕 # sys.argv[1] == "茅t茅馃悕"
bun run script.py # same through bun run
bun python -c "import sys; print(sys.version)"
bun python -m http.server 8000
echo "print('from stdin')" | bun python -
Tracebacks are printed by CPython and an uncaught exception exits with a non-zero status. A missing or incompatible host fails before any Python source is evaluated.
Embed CPython with bun:python
bun:python loads the host through bun:ffi and runs code in the __main__ module of an interpreter inside the current process (os.getpid() equals process.pid). The modules buv:python and pyjs:python are aliases.
import { Python, PythonError } from "bun:python";
using py = Python.open();
console.log(py.version()); // Py_GetVersion()
py.exec`import math; value = {"text": "caf茅馃悕", "n": 42}`;
console.log(py.evalJSON("value")); // { text: "caf茅馃悕", n: 42 }
console.log(py.eval("math.sqrt(2)")); // "1.4142135623730951" (str() of the result)
console.log(py.call("platform", "system")); // module.function(), returns str()
try {
py.eval("1 / 0");
} catch (error) {
if (error instanceof PythonError) console.log(error.status, error.message); // -4, "... division by zero"
}
| Method | Description |
|---|---|
Python.open(options?) | Loads the host and initialises the interpreter, or attaches to a running one. started tells whether this handle owns it. |
run(code), exec`code` | Runs statements in __main__. The tagged template takes raw source and refuses interpolation. |
eval(expression) | Evaluates an expression and returns str(result). |
evalJSON(expression) | Evaluates an expression, serialises it with Python's json module and parses it in JavaScript. |
call(module, fn, arg?) | Calls module.fn(arg) (without an argument when arg is omitted) and returns str(result). |
version() | Py_GetVersion(). |
close() / using | Finalises the interpreter if this handle started it, then closes the host library. |
Python.open() options:
libraryPath: a host library to load instead ofBUV_PYTHON_HOST_LIBRARY/BUN_PYTHON_HOST_LIBRARYor a host embedded in a compiled executable.libpython: the CPython library, ornullto use one already loaded. By default it comes fromBUV_PYTHON_LIBPYTHON,BUN_PYTHON_LIBPYTHONorAPHRODY_LIBPYTHON, then from the active buv artifact (BUV_RUNTIME, or$BUV_HOME/runtime/<target>/current, default~/.buv).
The helpers pythonHostLibraryPath(), buvLibpython() and libpythonIn(directory) expose this resolution.
Errors
Every failure throws a PythonError with a status from PyStatus:
PyStatus | Value | Meaning |
|---|---|---|
Ok | 0 | success |
Arg | -1 | invalid argument (for example a NUL character) |
Load | -2 | the host or libpython could not be loaded |
State | -3 | closed handle or interpreter state error |
Python | -4 | a Python exception; the message carries it |
Panic | -5 | a panic inside the host |
Unsupported | -6 | host ABI mismatch or unsupported operation |
Asynchronous calls
Python.async() (or PythonAsync.open()) runs the interpreter in a Worker of the same process, so long Python calls do not block the event loop. Calls are queued in order (up to 256 pending) and every method returns a promise.
import { Python } from "bun:python";
await using py = await Python.async();
await py.exec`import os; value = 40`;
const [a, b] = await Promise.all([py.eval("value + 2"), py.eval("value + 3")]);
console.log(a, b); // "42" "43"
Compile Python
bun compile <entry.py> (also buv compile and pyjs compile) compiles Python with an isolated native compiler resolved through the embedded UV: Cython 3.3.0 by default, or Nuitka 4.2.2.
bun compile app.py # native executable (Cython)
bun compile app.py --backend nuitka --onefile # one-file Nuitka executable
bun compile mod.py --format shared --module-name mod # CPython extension exporting PyInit_mod
bun compile app.py --zipapp # executable archive
bun compile --help
| Flag | Description |
|---|---|
--format exe|shared|wasm | Product kind. shared exports the PyInit_* extension ABI. |
--outfile <path> | Output path. An existing product is kept unless --force is given. |
--backend cython|nuitka | Native compiler (default Cython). |
--module-name <name> | Import name of the extension and of its PyInit_* symbol. |
--onefile | Nuitka standalone executable in a single file. |
--target native|wasm | Cross-platform products require a qualified host. |
--bytecode, --zipapp | Bytecode or executable-archive products. |
--db <bun_python.sqlite> | Record compilation receipts. |
--offline | Resolve compiler packages from the UV cache only. |
bun build app.py with the same flags takes the same path.
Cython executables need CPython and its standard library at run time. WASM output needs a real CPython WASI toolchain and fails without one; it never writes a partial artifact. Compilation never overwrites its source file.
PyJS files
.pyjs, .pyts and .pytsx modules use Bun's JavaScript, TypeScript and TSX loaders. Python code inside them still goes through bun:python explicitly.
import { value } from "./value"; // value.pyjs, value.pyts or value.pytsx
console.log(value);
CPU-only vLLM
scripts/aphrody/vllm-cpu.ts builds and launches the aphrody branch of aphrody-labs/vllm (a fork of vllm-project/vllm) on the CPU backend, in a virtualenv created by UV with the embedded CPython.
| Command | Effect |
|---|---|
bun scripts/aphrody/vllm-cpu.ts env | Prints the tuned environment: OMP_NUM_THREADS = physical cores, OMP_PROC_BIND=close, OMP_PLACES=cores, VLLM_CPU_OMP_THREADS_BIND=auto, LD_PRELOAD of tcmalloc or jemalloc when installed, BUN_PYTHON_EXECUTABLE of the virtualenv. |
bun scripts/aphrody/vllm-cpu.ts run <args> | Runs the OpenAI-compatible server with that environment. |
bun scripts/aphrody/vllm-cpu.ts update | Fetches upstream/main, rebases aphrody on it, rebuilds only when the sha differs from built.sha, replacing the previous virtualenv. A lock directory keeps a single build in the queue; the build runs under nice -n 15 with half the cores. |
bun scripts/aphrody/vllm-cpu.ts bench <model> | Reports tokens/s with the default environment, then the tuned one. |
The vLLM CPU backend compiles an AVX2 kernel set (cmake/cpu_extension.cmake) next to the AVX-512 and AMX sets, so an AVX2-only host (Haswell) works with the default build. The kernels are selected at build time from the host flags; build on the machine that serves, or on one with the same ISA. Run update from a timer on the build host only; the serving host receives the virtualenv, never builds. Tokens/s figures are not recorded yet.