pythond

Persistent Python sessions. Code in, result out.

Variables, connections, and threads survive between calls. Agents send code through one-shot commands. Humans attach a REPL. Same namespace.

pip install pythond        # zero dependencies

Python 3.10+. Three entry points:

CommandRoleLike
pythonddaemonsshd
pyshsession clientfunction call / attach
pyctldaemon controlsystemctl

The whole idea

ns = {}
while True:
    code = receive()
    exec(code, ns)         # ns stays alive -- variables survive
    send(captured_stdout)

Everything pythond adds is that loop plus delivery: thread-safe stdout capture, REPL semantics (the last expression auto-prints), one subprocess per named session, async cells, and local HTTP so one-shot CLI calls reach the live process. Transport is borrowed, never built — there is no WebSocket stack, no TLS stack, no PTY bridge, and no remote proxy in the codebase.

Quick start

pythond daemon
pysh new work
pysh run work "x = 42"
pysh run work "x + 1"
# 43
pysh attach work

State persists

pysh run work "import sqlite3; db = sqlite3.connect('app.db')"

# ... 100 turns later ...

pysh run work "db.execute('SELECT count(*) FROM users').fetchone()"
# (42,)

Connection ≠ state. Every call is a fresh connection to the same live process. The namespace is the workspace.

Async execution

# fire: thread, shares namespace
pysh fire work "model = train(X, y)"
pysh poll work abc123
# {"cell_id":"abc123", "status":"done", "output":"..."}
pysh run work "model.score(X_test)"       # model is there

# fork: child process (POSIX only), killable, pickles vars back
pysh fork work "results = expensive_search(params)"
pysh int work                              # SIGKILL the fork (POSIX)

Post a file

Complex code with quotes, f-strings, or SQL? Post the file as the cell. The request body is raw Python source — never JSON-escaped, never shell-quoted.

cat > /tmp/task.py << 'EOF'
import pandas as pd
df = pd.read_csv("data.csv")
print(f"rows: {len(df)}, cols: {list(df.columns)}")
EOF

pysh run work @/tmp/task.py

Remote = ssh

State lives in the remote daemon, not in the connection, so one-shot calls over ssh are enough:

ssh server pysh run work "x = 42"
ssh server pysh run work "x + 1"     # 43 (remote state)

ssh -t server pysh attach work       # interactive

Per-call latency? That is what ssh ControlMaster is for:

# ~/.ssh/config
Host server
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m

Need a TLS endpoint anyway? Terminate it with nginx or caddy in front of the loopback port. pythond does not ship a TLS stack.

HTTP API

pysh speaks plain HTTP over a local socket; so does curl:

curl --unix-socket $XDG_RUNTIME_DIR/pythond/pythond.sock \
     --data-binary '1 + 1' http://pythond/run/work        # 2

curl --unix-socket ... --data-binary @task.py http://pythond/run/work
GET  /ls                      text listing
POST /new/<name>              create session
POST /run/<name>    body=code raw output; X-Pythond-Exec-Error: 1 on traceback
POST /fire/<name>   body=code {"cell_id": ..., "status": "fired"}
POST /fork/<name>   body=code {"cell_id": ..., "status": "forked"}
GET  /poll/<name>[?cell=ID]   JSON cell result
GET  /status/<name>           JSON health
GET  /vars/<name>             JSON namespace names
POST /complete/<name> body    JSON completion matches
POST /int/<name>              JSON interrupt report
GET  /pickle/<name>[/<var>]   pickled var (or whole picklable namespace)
POST /pickle/<name>[/<var>]   unpickle body into var (or merge a dict)
POST /kill/<name>             kill session
POST /stop                    stop daemon

404 no such session, 409 session channel broken, 401 bad token.

Objects move as pickles

run moves source code; /pickle moves live objects — the fork merge-back mechanism, generalized. pysh cp gives it scp syntax: a side is session:var, session: (the whole picklable namespace), or a file path.

pysh cp work:df df.pkl          # session -> file
pysh cp df.pkl gpu:df           # file -> session
pysh cp work:model gpu:model    # session -> session
pysh cp work: backup:           # clone the picklable namespace

Unpicklable values (sockets, locks, modules) are skipped and reported. POSTing a pickle is arbitrary code loading by design — the same trust boundary as /run.

Attach

pysh attach work is a client-side line REPL: readline history and tab completion live in the client, every complete block runs as one cell in the shared namespace. Ctrl-D detaches; the session stays alive (pysh kill ends it). It is line-oriented, not a PTY.

Commands

pysh new <name>              create session
pysh run <name> "code"       sync exec, raw output
pysh run <name> @task.py     post a file's contents as the cell
pysh fire <name> "code"      async thread, shares namespace (can't kill C code)
pysh fork <name> "code"      async process (POSIX), killable, pickles vars back
pysh poll <name> [cell_id]   check async result
pysh int <name>              interrupt (fire=best effort, fork=kill)
pysh kill <name>             terminate session
pysh ls                      list sessions
pysh status <name>           JSON health
pysh vars <name>             JSON namespace names
pysh complete <name> "text"  JSON completion candidates
pysh attach <name>           line REPL (Ctrl-D to detach)
pysh cp <src> <dst>          copy pickled objects (scp syntax)

pyctl start [--show-token]   start daemon in foreground
pyctl stop                   stop daemon
pyctl status                 daemon liveness

Security

Treat pythond like SSH into a Python runtime. Not a sandbox: code runs with the daemon user's OS permissions. Once connected, a client has full access to all sessions — the same as a login shell.

ModeAuth
Local POSIXAF_UNIX socket, mode 0o600 — filesystem permissions
Local Windows127.0.0.1 + bearer token in %LOCALAPPDATA%\pythond
Remotessh's problem, on purpose

The daemon never binds a non-loopback address. There is no network listener to harden.

Session names are lowercase only: a-z, 0-9, underscore, and hyphen, up to 80 characters. Windows reserved device names such as CON, NUL, COM1, and LPT1 are rejected.

Like shell history under SSH, pythond session history and live namespaces can expose secrets. history.py may contain executed Python source. Do not paste API keys, passwords, tokens, or other secrets into cells unless you are willing for them to persist in that session and its local files.

Platform

PlatformTransportNotes
Linux / macOS / WSLHTTP over AF_UNIXfull featured
WindowsHTTP over 127.0.0.1 + tokenfull featured except fork (no COW fork)

Dependencies

None. Python standard library only.


Source · PyPI · MIT license