← LOGBOOK LOG-413
WORKING · SOFTWARE · ·
MARIMOPYTHONNOTEBOOKSREACTIVEUVDATA-SCIENCE

Marimo: Reactive Python Notebooks

First look at Marimo — a reactive notebook runtime for Python that treats notebooks as pure Python files and reruns cells automatically on change.

What Marimo Is

Marimo is a reactive Python notebook — when you change a cell, every downstream cell that depends on it reruns automatically. No hidden state, no out-of-order execution surprises. The thing that makes it different from Jupyter isn’t the UI, it’s the execution model.

Each notebook is a valid .py file. You can run it as a script (python notebook.py), serve it as a web app (marimo run), or open it in the editor (marimo edit). No .ipynb JSON blobs, no metadata drift.

Setup

uv add marimo
marimo edit

uv add drops it into the project’s virtual environment without touching global state. marimo edit opens the browser-based editor at localhost:2718.

The Reactive Model

In Jupyter, cells are manually ordered and executed. You can run cell 5 before cell 3 and end up with stale state that doesn’t match what the code says. Marimo solves this with a dependency graph: if cell B uses a variable defined in cell A, Marimo knows that and reruns B whenever A changes.

Cell A: x = 10       ← change this
Cell B: y = x * 2    ← automatically reruns
Cell C: print(y)     ← automatically reruns

This also means: no mutable global state between cells. Each cell is a function. If you try to assign the same variable in two cells, Marimo flags it as a conflict.

UI Elements Built In

Marimo has first-class UI widgets — sliders, dropdowns, text inputs — that integrate with the reactive graph. A slider value changing is the same as a cell output changing: everything downstream reruns.

slider = mo.ui.slider(1, 100, value=10)
slider
result = slider.value ** 2
result

Move the slider → result updates instantly. No callbacks, no event handlers.

Notebook as App

marimo run notebook.py serves the notebook as a web app — UI elements are visible, code cells are hidden. This is a clean path from exploratory analysis to shareable tool without rewriting anything.

What’s Different From Jupyter

JupyterMarimo
File format.ipynb (JSON).py (pure Python)
ExecutionManual, orderedReactive, automatic
Hidden stateCommonImpossible by design
Git diffsMessyClean
Run as scriptNeeds nbconvertNative

Wiring Up a Local LLM Chat

mo.ui.chat takes a callback that receives the message history and returns a string. Plugging Ollama in is a few cells:

Cell 1 — imports:

import os
from dotenv import load_dotenv

Cell 2 — load env, returns True on success:

load_dotenv()

Cell 3 — marimo:

import marimo as mo

Cell 4 — http client:

import requests

Cell 5 — endpoint from env, with fallback:

OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")

Cell 6 — the respond callback:

def ollama_respond(messages):
    last = messages[-1].content
    res = requests.post(f"{OLLAMA_URL}/api/generate",
                        json={
                            "model": "llama3.1:8b",
                            "prompt": last,
                            "stream": False
                        })
    return res.json()["response"]

Cell 7 — wire callback into chat widget:

chat = mo.ui.chat(ollama_respond)

Cell 8 — render:

chat

The chat widget handles the UI entirely — input box, message history, scroll. Your callback only needs to return a string. This pattern — reactive cells + a local model — makes for a clean local AI playground. No server setup beyond Ollama, no API keys, and the notebook stays a plain .py file.

First Impressions

The execution model is the right one — Jupyter’s hidden state has caused real bugs in data pipelines. Marimo removes that class of problem by construction. The trade-off is that you have to think more carefully about variable scope upfront, which is actually good discipline.

The .py file format is a genuine win for version control. Diffing a notebook change is actually readable.