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:

keyvalue
outputcaptured stdout (str | None)
displayrich values: text/plain, text/html, image/png base64, …
errortraceback / warning text (str | None)
execution_countincrementing cell number
execution_timeseconds (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

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 & pathpurpose
POST /sessionscreate 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 /sessionslist live sessions
GET /sessions/{id}/statsper-session usage snapshot
DELETE /sessions/{id}destroy session
GET /healthliveness {"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.