Skip to content

Execution model

Molt gives each tab a long-lived Python subprocess for the duration of that kernel session.

A tab’s cells share one global namespace in one process. Assignments, imports, function definitions, and object mutations remain available to later executions in that tab:

records = []
records.append({"status": "ready"})

A later cell can evaluate:

records
[{'status': 'ready'}]

Another tab cannot see records, because it uses another process with another globals dictionary.

The backend keeps a map from tab index to kernel instance.

  • Tab 1 is ensured during application startup.
  • A different tab is ensured when it becomes active.
  • Execution also ensures that a kernel entry exists before sending code.
  • A stopped sentinel is not automatically replaced; the user must Restart.

All four processes use the same interpreter captured when Molt started.

Each kernel has a bounded asynchronous request channel with capacity 64. Requests are consumed in first-in, first-out order by one I/O loop, so a tab executes one cell at a time.

Different tabs have different channels and processes, so they can run concurrently.

The frontend’s Run All procedure adds its own sequencing: it awaits each nonempty cell before requesting the next.

Python parsing and final-expression display

Section titled “Python parsing and final-expression display”

The bundled server parses a cell with Python’s ast module in exec mode.

  1. If the final statement is a bare expression, the server removes it from the statement list.
  2. It executes all preceding statements with exec.
  3. It evaluates the final expression with eval.
  4. If the value is not None, it stores the value as _ and prints repr(value).

This is why a cell ending in items displays the representation of items, while a cell ending in items.append(value) displays nothing when append returns None.

This is plain Python behavior, not IPython. Magics, shell escapes, top-level notebook helpers, and rich display hooks are absent.

During execution, the kernel temporarily replaces sys.stdout and sys.stderr with in-memory text buffers. It returns:

  • Captured standard output.
  • Captured standard error.
  • A formatted traceback for ordinary exceptions.
  • An output type currently fixed to text.

The frontend converts those fields into separate stream or error records. Because stdout and stderr are buffered separately, their original interleaving is not preserved; the frontend renders stdout first, then stderr, then an error traceback.

During cell execution, sys.stdin is replaced with an empty text stream so user code cannot consume the JSON control channel. Calls to input() raise EOFError.

Ordinary exceptions are caught and returned as text. The Python process remains available, and mutations that occurred before the exception may remain in the namespace.

KeyboardInterrupt is converted to a short error string. The Rust interrupt path schedules a force kill after two seconds only if the same execution is still busy; a completed interrupt response preserves the kernel.

SystemExit and other BaseException subclasses are returned as cell errors, so calls such as sys.exit() do not terminate the kernel server.

  • Restart kills the process and creates a fresh one for that tab.
  • Stop kills the process and leaves a stopped marker that blocks execution until Restart.
  • App shutdown drains the kernel map and kills every remaining child process.
  • Child processes are also configured with kill-on-drop as an additional cleanup boundary.

No kernel namespace is serialized. Reproducibility requires restarting and rerunning source cells in order.