py-runner-kernel
Persistent Python code runner: string in, JSON out.
A lightweight, dependency-free, ipykernel-like execution library — no Jupyter server, no ZMQ, no required third-party packages. Zero runtime dependencies, Python ≥ 3.10.
from py_runner import Kernel
k = Kernel()
k.execute("x = 40 + 2")
print(k.execute("x + 1"))
# {"output": null,
# "display": [{"type": "text/plain", "data": "43"}],
# "error": null, "execution_count": 2, "execution_time": 0.0001}
Install
pip install py-runner-kernel
Quickstart
from py_runner import Kernel, execute, execute_json
# Module-level shared kernel
execute("import math")
print(execute_json("math.sqrt(16)"))
# Isolated session kernel (one per user/request)
k = Kernel(on_input=lambda prompt: "bob", max_history=100)
r = k.execute('name = input("who? ")\nname.upper()')
assert r["display"][0]["data"] == "'BOB'"
Every result is a JSON-serializable dict:
| key | value |
|---|---|
output | captured stdout (str | None) |
display | rich values: text/plain, text/html, image/png base64, … |
error | traceback / warning text (str | None) |
execution_count | incrementing cell number |
execution_time | seconds (float) |
API reference
Kernel(...)
Kernel(
on_input=None, # default callback for input(prompt)
max_history=100, # cap on Out[N] entries (oldest evicted)
allow_shell=True, # permit !cmd escapes
allow_pip=True, # permit %pip installs
)
execute(...)
k.execute(
code,
max_output_length=10000,
max_display_length=50000,
on_stream=None, # live stdout chunks
on_input=None, # per-call input() callback
timeout=None, # seconds; TimeoutError past N
heartbeat_interval=None, # ping during silent fit() loops
on_heartbeat=None,
)
Helpers
execute_json(code)— same result as a JSON stringk.interrupt()— stop the running cell (cooperative)k.reset(keep_imports=False)— wipe the namespacek.clear_history()— dropOut[N]entriesk.stats()— execution count, history size, namespace bytesset_address_space_limit(mb)— Unix memory cap (MemoryError instead of OOM)
Magics
%time · %timeit · %who / %whos ·
%run file.py · %pip install … · !shell-cmd · %reset.
A trailing ; suppresses last-expression display.
CLI
py-runner -c "print(2 + 3)"
py-runner analysis.py --timeout 300 > result.json
cat cell.py | py-runner
python -m py_runner -c "1+1"
py-runner -c "x" --no-shell --no-pip
HTTP server
py-runner-serve --host 127.0.0.1 --port 8000 --max-sessions 100
| method & path | purpose |
|---|---|
POST /sessions | create session → {"session_id": …} (server UUID, 201; 429 past cap) |
POST /execute | {session_id, code, timeout?} → result JSON |
POST /interrupt | {session_id} → stop the running cell |
GET /sessions | list live sessions |
GET /sessions/{id}/stats | per-session usage snapshot |
DELETE /sessions/{id} | destroy session |
GET /health | liveness {"status": "ok"} |
S=$(curl -s -X POST localhost:8000/sessions | python3 -c "import sys,json; print(json.load(sys.stdin)['session_id'])")
curl -s -X POST localhost:8000/execute -d "{\"session_id\":\"$S\",\"code\":\"x = 40 + 2\"}"
curl -s -X POST localhost:8000/execute -d "{\"session_id\":\"$S\",\"code\":\"x + 1\"}"
curl -s -X DELETE localhost:8000/sessions/$S
Sessions are isolated per ID and live until deleted (no idle timeout).
Kernels default to allow_shell=false, allow_pip=false;
pass {"allow_shell": true} to POST /sessions to relax per session.
Acode integration
The jupyter-acode extension
renders .ipynb notebooks on-device and executes cells through this server:
one server session per open notebook, Run / Run-all / Interrupt controls, and outputs
persisted back into the notebook file. Install the backend once from the extension
(pip install py-runner-kernel, globally or into a /home venv) —
the server starts itself under the hood on first run.
License
AGPL-3.0-or-later. Note the network clause (§13): if you serve a modified version over a network, you must offer its source to those users.