GitHub - BartlomiejLewandowski/looseleaf: Minimal filesystem-backed Python notebooks. Each cell is a .py file.

5 min read Original article ↗

Filesystem-backed Python notebooks. Each cell is a .py file. Output lives in .output sidecar files. One long-running kernel holds shared state.

Install

Requires Python 3.10+.

git clone https://github.com/BartlomiejLewandowski/looseleaf
cd looseleaf
pip install .

Usage

looseleaf serve [dir]        # default dir is current directory
looseleaf serve my-experiment
looseleaf serve ./analysis --port 9000

Point it at a directory. If it doesn't exist, it gets created. Open http://localhost:8765.

Only one kernel can run per directory at a time. A .looseleaf.lock file is written on startup and removed on exit. Starting a second kernel in the same directory prints an error and exits.

CLI commands (requires a running kernel)

looseleaf cells              # list cells and whether each has output
looseleaf run 02_load.py     # run one cell, print output, exit 0/1
looseleaf run-all            # run all cells in filename order
looseleaf reset              # clear the shared namespace
looseleaf interrupt          # stop the currently running cell

All commands accept --port PORT.

Development

./dev [dir]   # restarts the kernel automatically when kernel.py or index.html change

How it works

The kernel is a single Python process that:

  • Maintains a shared namespace — variables persist across cells, just like Jupyter
  • Executes .py files on demand via exec() in that namespace
  • Streams output over WebSocket as it happens (stdout, stderr, matplotlib plots)
  • Writes .output JSON sidecar files alongside each .py cell
  • Watches the filesystem for external edits (edit cells in your editor, changes appear in the browser)
  • Serves the frontend on the same port

Cells are just Python files. They sort alphabetically, so use numbered prefixes:

my-experiment/
├── 01_setup.py
├── 01_setup.output
├── 02_load_data.py
├── 02_load_data.output
├── 03_train.py
└── 03_train.output

Keyboard shortcuts

Key Mode Action
Enter command Enter edit mode
Esc edit Back to command mode
Cmd+Enter both Run cell
Shift+Enter both Run cell, select next
Up/Down command Navigate cells
j/k command Navigate cells (vim-style)
Up at line 1 edit Select previous cell
Down at last line edit Select next cell

Two modes, like Jupyter:

  • Command mode (blue left bar) — cell is selected, keyboard navigates between cells
  • Edit mode (green left bar) — cursor is in the editor, keyboard types code

Features

  • Streaming output — print statements and plots appear in real time
  • Shared state — cell 2 can use variables from cell 1
  • Monaco editor — syntax highlighting, autocomplete, the same editor as VS Code
  • Filesystem is the source of truth — cells are real .py files, work with git/linters/editors
  • .output files — JSON with stdout, stderr, error tracebacks, base64 images, timing
  • Live stats — memory usage, variable count, uptime in the toolbar
  • File watcher — edit cells externally, browser updates automatically
  • Multi-client — multiple browser tabs see the same kernel state

.output format

{
  "stdout": "Hello world\n",
  "stderr": "",
  "images": ["base64-encoded-png..."],
  "error": null,
  "elapsed": 0.003,
  "execution_number": 3
}

Add *.output to .gitignore. They're regenerated on run.

Limitations

  • One kernel, one namespace, one directory. That's the point.
  • No cell reordering in the UI — rename files to reorder (they sort alphabetically).
  • No markdown cells — write comments in Python instead.
  • Matplotlib only for plot capture — uses the Agg backend, patches plt.show().
  • Editing conflicts with multiple users — last save wins.
  • One kernel per directory — starting a second kernel in the same directory is an error.

AI-friendly by design

Because cells are plain files, AI coding agents (Claude Code, Copilot, Cursor, etc.) can work with looseleaf notebooks naturally:

  • Edit one cell at a time — each cell is its own .py file. An agent rewrites 03_train.py without touching anything else. No monolithic JSON to parse or accidentally corrupt.
  • Read output without running anything.output sidecar files are plain JSON. An agent can inspect stdout, stderr, error tracebacks, and timing from the last run before deciding what to change.
  • Drop in new cells — create a new .py file in the directory and the file watcher picks it up automatically. No API, no restart.
  • Standard Python — no special syntax, no cell markers, no notebook format. Any tool that understands .py files already understands a looseleaf notebook.

Further reading

Related projects

  • marimo — Reactive Python notebook stored as pure .py files, git-friendly, Monaco editor, streaming output. Much heavier and more opinionated (reruns dependent cells automatically), but the closest in spirit. No one-file-per-cell model.
  • srush/streambook — Tiny (~50 lines): watches a .py file and renders a live Streamlit view. Same minimal philosophy, but uses # %% markers in a single file rather than separate files per cell.
  • netgusto/nodebook — Multi-lang web REPL where a notebook is a directory containing a single main.py. Same "directory = notebook" concept, but no multi-cell shared-state model.
  • jupytext — Syncs .ipynb notebooks to plain .py files using # %% markers. Git-friendly, edit in any editor — but depends on Jupyter as the execution backend.
  • nteract/hydrogen — Atom plugin for running Jupyter kernels inline in text files. Same "edit in real editor" idea; now unmaintained since Atom was sunsetted.