live lazy variable inspector for python
Project description
paar
paar is a live, lazy variable inspector for Python: a PyCharm-style variables pane
for any running interpreter — Jupyter, a plain script, or a training run — served over HTTP
from inside the process, with web, notebook, and terminal frontends.
- Live: the view refreshes after every cell (IPython) or by poll+diff (any process).
- Lazy: children are resolved one level at a time, on click — a million-row DataFrame costs nothing until you open it, and then only one page.
- Everywhere: the same server feeds the inline Jupyter panel, the browser,
paar-tuiin a terminal, and — new — a restricted MCP surface for AI agents, so Claude Code or Codex can work inside your live session without being able to break it.
Installation
pip install paar # from pypi
pip install paar[mcp] # + the paar-mcp server for agent access (Claude Code, Codex, ...)
pip install git+https://github.com/vedicreader/paar.git # latest from GitHub
Docs are hosted on this repo’s pages.
Live demo
from paar.fasthtml import inspector
inspector() # panel renders inline; keep this cell's output visible
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
if(event.detail.path.includes('://')) return;
htmx.config.selfRequestsOnly=false;
event.detail.path = `${location.protocol}//${location.hostname}:8000${event.detail.path}`;
});
</script>
<iframe src="http://localhost:8000/" style="width: 100%; height: 520px; border: none;" onload="" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
import math, numpy as np, pandas as pd
data = {'a': 1, 'b': [1, 2, {'deep': [10, 20]}], 'c': 'hello', 'pi': math.pi}
arr = np.arange(12).reshape(3, 4)
df = pd.DataFrame({'x': range(5), 'y': list('abcde')})
data['d'] = list(range(1000)) # run this; the panel above updates without re-running inspector()
Viewing modes (all from the one in-kernel server):
- Inline: the
inspector()cell output above updates live after every cell — you do not re-run it. - Docked side panel (JupyterLab): right-click the
inspector()output → Create New View for Cell Output → drag into a split pane. - Full window: open http://localhost:8000/ or click the open in browser link.
Click the ▸ toggle on any container row (dict / list / tuple / set / object) to load its
children one level at a time. Nested containers keep their own toggles, so you can drill in
as deep as the data goes — e.g. data → 'b' → [2] → 'deep'.
arr (numpy) and df (pandas) show a ▦ grid toggle instead of a tree — click it to open a
scrollable, paged table (use the row/col ◀ ▶ controls for data larger than one page). Plain
containers still expand as a tree.
Exec, inline edit, and filtering:
- Exec box: type an expression or statement and press Run — executes in the kernel; tick Isolated to evaluate side-effect-free (result shown without writing back to the namespace).
- Inline edit: click any scalar value in the tree to edit it in place; the new value is written back into the kernel namespace immediately.
- Filter bar: narrow the variable list by name substring or type (use the name field and the type dropdown above the table).
Across processes and environments
The inspector is not limited to Jupyter. Any plain Python program (in any package or virtualenv) can start a background paar server and expose its own live namespace:
import paar
paar.fasthtml.serve() # inspects the caller's module globals; returns the URL, e.g. http://127.0.0.1:8000
serve() runs the server on a daemon thread and refreshes the view by polling the namespace, so no
Jupyter kernel or manual nudging is needed. Each server registers itself (by repo/package name) in a
small local registry, so multiple environments can run paar at once and any frontend can discover
and switch between them.
From the command line
Run any script under a live inspector — the view updates while the script runs, and the server stays up afterwards so you can inspect the final state:
$ uv run paar-serve train.py # or: uvx --from paar paar-serve train.py
paar inspector live at http://127.0.0.1:8000 (connect a terminal with: paar-tui)
Terminal frontend
paar-tui is a gruvbox terminal inspector (built with Textual) that connects to any running server —
useful across environments since it only needs HTTP/WS:
$ uv run paar-tui # auto-discovers a running server from the local registry
$ uv run paar-tui --env train # pick a specific env by name
$ uv run paar-tui --url http://host:8000
Inside the TUI: / filters variables, x opens a code-exec bar, 1/2/3 switch profile, g opens
the grid, e switches between live environments, r refreshes, q quits.
Sharing your session with AI agents
Your session is yours; an agent’s access to it should not be. serve() and inspector()
also publish a restricted agent surface (/agent/api/*) built on two mechanisms in
paar.agent:
- An overlay namespace (
AgentSession). Agent code reads your variables directly, but every name it binds lands in its own persistent layer — assigning to one of your names merely shadows it, and deleting one is impossible by construction. Your namespace is never written. - A restriction list (clikernel-compatible
inspectors). Before a cell runs, its AST is checked: in-place mutation of your objects
(
.append,df.drop(inplace=True),x += ...),exec/eval-style escapes, and destructive file/shell calls are refused with an error that tells the agent what to do instead.
The net contract: agents can read anything and create anything new; they can never change or delete what you made. It works standalone too:
from paar.agent import AgentSession
yours = {'rows': [1, 2, 3], 'threshold': 2} # stand-in for your live namespace
agent = AgentSession(owner=lambda: yours, rules=[]) # rules=[]: skip any local inspectors.py
agent.run('kept = [r for r in rows if r >= threshold]') # reads yours, creates its own
agent.layer
for cell in ('rows.append(99)', 'del threshold', 'rows = []'):
r = agent.run(cell)
print(r.error or f'{cell!r} ran; owner still sees {yours}')
'rows = []' succeeded — as a shadow: the agent now has its own rows, while yours
is untouched and still what you see. You can watch everything the agent has made via
paar.fasthtml.agent_layer() (assign it to a variable and it shows up, expandable, in your
own panel). Server modes: serve(agent='restricted') (default), 'readonly' (every agent
exec is isolated — nothing persists), or 'off'. Add your own rules in
~/.config/paar/inspectors.py (or $PAAR_INSPECTORS) using clikernel’s file contract:
define inspect(tree[, code]) and/or an inspectors list; raise
paar.agent.RuleBlock to refuse a cell.
Owner endpoints (/exec, /set, /api/exec) are gated separately by a per-server owner
token (<reg_dir>/token-<port>, mode 0600). Your web panel and paar-tui pick it up
automatically; agents on /agent/api never see it.
Connecting Claude Code or Codex
paar-mcp is a stdio MCP server exposing the agent surface
as tools — list_vars, expand, grid, execute, session_info — and finds your server
through the local registry:
# your session (Jupyter or script): # Claude Code:
from paar.fasthtml import inspector claude mcp add my-session -- paar-mcp --env myrepo
inspector() # Codex (config.toml):
# [mcp_servers.my-session]
# command = "paar-mcp"
# args = ["--env", "myrepo"]
--env picks a registry entry by name (defaults to the only live one); --url pins a base
URL instead. The server resolves its target per call, so start order doesn’t matter.
skills/paar-session/SKILL.md documents the contract for agents — copy it into
~/.claude/skills/ (or your agent’s skill directory) so the agent knows the etiquette
before its first call.
Pairing with clikernel: watching the agent’s own session
The mirror-image setup: while paar-mcp lets an agent into your session, paar can also ride
inside the agent’s clikernel worker — the
persistent Python process Claude Code/Codex use as their workbench — so you can watch and
steer their session live.
clikernel runs ~/.config/clikernel/startup.py inside the worker via %run -i, which is all
paar needs:
# ~/.config/clikernel/startup.py — runs inside every clikernel worker
import os
import paar.fasthtml
paar.fasthtml.serve(port=8001, name=os.environ.get('PAAR_ENV_NAME', 'claude'))
- Two sessions, two ports: your kernel on
inspector(port=8000), the agent’s worker on 8001 (serveauto-bumps if taken). Both register in the local registry, sopaar-tui’sekey or the web env dropdown flips between you and claude. - Naming: set
PAAR_ENV_NAME=claude/PAAR_ENV_NAME=codexin each client’s MCP server config for clikernel, so the one startup.py labels every worker correctly. - Restarts survive: clikernel’s
restarttool spawns a fresh worker, which re-runs startup.py — the inspector comes back by itself. - One rules file, two enforcers: paar’s inspectors use clikernel’s exact contract
(
inspect(tree[, code]),inspectors,RuleBlock), so the restriction list you write for the agent’s own kernel (~/.config/clikernel/inspectors.py) can be reused for its access to your session (~/.config/paar/inspectors.py).
Full circle: Claude works in its clikernel on 8001 while reaching into your 8000 session
read-and-create-only via paar-mcp — and you see both, live, from one TUI.
Security model (honest edition)
These are guardrails for well-behaved agents, not a sandbox. Concretely:
- The overlay gives a structural guarantee for name bindings: agent code cannot rebind or delete your variables no matter what it writes.
- The AST restriction list guards in-place mutation and escapes, but static analysis can be
evaded by sufficiently creative code (
getattrchains, new mutator names, …). - The owner token keeps well-behaved tools off the owner endpoints, but any same-user local process with shell access could read the token file or the served page.
The realistic threat here is an agent accidentally clobbering your state — which this stops cold. A hard boundary against adversarial code would need a separate process on a copy of the data, which defeats the point of a live shared session.
Developer guide
paar is an nbdev project: the notebooks in nbs/ are the source of
truth, paar/*.py is generated. To hack on it:
pip install -e . # editable install
# edit notebooks under nbs/ ...
nbdev_prepare # export modules, run tests, clean notebooks
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paar-0.0.1.tar.gz.
File metadata
- Download URL: paar-0.0.1.tar.gz
- Upload date:
- Size: 47.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00cd8be2a8ec0437fe923ab04c399657d49c744b37dfabc5924c670c512edab7
|
|
| MD5 |
c1b9a4d7d64e343bbc8a3f9190d64b7b
|
|
| BLAKE2b-256 |
c8062526f6f29cbc7289e56056360290285c2dbda7f04e0e804ead3d1db9e3c3
|
File details
Details for the file paar-0.0.1-py3-none-any.whl.
File metadata
- Download URL: paar-0.0.1-py3-none-any.whl
- Upload date:
- Size: 54.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f0f2e5508fe631bf0634040bc16bcd6e16f8febb5f5bb9543455c6b102417f64
|
|
| MD5 |
f24d010ceb38e55bde8b13315499a733
|
|
| BLAKE2b-256 |
d96f66c52a919a339c0ba4bc7c8117ac6182bc8bca47b1b9d0397ab43581d419
|