Marimo

A python visual notebook that works like I imagined scientific notebooks should

2024-11-04 — 2025-09-09

Wherein a Python notebook format is described, stored as plain .py files and enforced to run deterministically to maintain reproducibility, UI controls being synchronised and execution order being topological.

faster pussycat
premature optimization
python
UI

Assumed audience:

People interactively developing code on annoying remote clusters

Figure 1

marimo is a Python-specific computational notebook that mostly just works. In particular, it solves many pain points of Jupyter (HT Jean-Michel Perraud). I think Jupyter is an ongoing disaster — a tarpit filled with problems that are each individually poisonous and on fire.

1 Value proposition

The FAQ explains it well, but I can summarise: tl;dr: Marimo is a different, imperfect compromise between the needs for reproducibility and reliability. Its abstractions are less likely to spill on my trousers than Jupyter’s are, while being more interactive than a pure Python script.

marimo solves problems in reproducibility, maintainability, interactivity, reusability, and shareability of notebooks.

Reproducibility. In Jupyter notebooks, the code you see doesn’t necessarily match the outputs on the page or the program state. If you delete a cell, its variables stay in memory, which other cells may still reference; users can execute cells in arbitrary order. This leads to widespread reproducibility issues. One study analysed 10 million Jupyter notebooks and found that 36% of them weren’t reproducible.

In contrast, marimo guarantees that your code, outputs, and program state are consistent, eliminating hidden state and making your notebook reproducible. marimo achieves this by intelligently analysing your code and understanding the relationships between cells and automatically re-running cells as needed.

Maintainability. marimo notebooks are stored as pure Python programs (.py files). This lets you version them with git; in contrast, Jupyter notebooks are stored as JSON and require extra steps to version.

Interactivity. marimo notebooks come with UI elements that are automatically synchronised with Python (like sliders, dropdowns); e.g., scrub a slider and all cells that reference it are automatically re-run with the new value. This is difficult to get working in Jupyter notebooks.

Reusability. marimo notebooks can be executed as Python scripts from the command line (since they’re stored as .py files). In contrast, this requires extra steps to do for Jupyter, such as copying and pasting the code out or using external frameworks. In the future, we’ll also let you import symbols (functions, classes) defined in a marimo notebook into other Python programs/notebooks, something you can’t easily do with Jupyter.

Shareability. Every marimo notebook can double as an interactive web app, complete with UI elements, which you can serve using the marimo run command. This isn’t possible in Jupyter without substantial extra effort.

The prices we pay:

  1. Marimo is less widely supported. Jupyter is everywhere.
  2. Unlike Jupyter, Marimo does not store the output of cells, so we can’t see the output of a cell without running it (unless we introduce our own explicit caching). This is a loss, true, but that supposed “feature” of Jupyter has caused me more pain than joy, so I do not miss it. [traumatic flashback to purging a gigabyte-sized notebook from my team git repo]. Caching should be explicit, not impossible to avoid.
  3. The “topological” execution order of cells can be confusing because it is not what Python traditionally does, although it is the only way to keep a notebook consistent. Note that notebook cells can appear in any order on the page, but they may execute in a totally different and sometimes surprising order (for example, if we made a typo and defined a variable somewhere foolish).
  4. The browser UI is pretty good (better than Jupyter), but not quite as polished as my VS Code setup. The VS Code integration is a bit janky — not quite as good as the native browser version (with all due respect to the developer!).
  5. Marimo has a VS Code integration, not quite as fancy as the Jupyter integration; make sure you use their recommended settings
  6. To keep execution order deterministic and names consistent, we can’t change the referent of a variable between cells. That would be fine in a functional language but is kind of tedious in Python, whose programming patterns depend on changing variable referents; this results in lots of awkwardly named things like experiment1, experiment2, etc. There are patterns to work around it but they are not idiomatic in Python.
  7. … Not sure yet. I’ll note problems as I discover them.

Places where marimo’s trade-offs are likely to be worthwhile for me:

  1. Development of code on HPC clusters where we want interactivity and persistence.
  2. Sharing code in a literate/exploratory way, i.e. with colleagues or students.
  3. Maybe building dashboards?

Places where I might prefer Jupyter:

  1. If I were working on some system that uses Jupyter but bans Marimo. This might arise in situations like Google Colab, where Jupyter is the primary interface, or in other turnkey data-science systems. A lot of people drank the Jupyter Kool-Aid.

Places where I would use neither:

  1. When I am working inside VS Code on my local machine and have my IDE set up just how I like it, and have no need to share with others. Then I’ll use that and plot using the local GUI infrastructure and the local AI coding assistants.

Note that this leaves not much of a niche for Jupyter in my life.

2 Installation

pip install marimo

See Getting Started with marimo for other options.

3 IDE integration

IDE integration has been marimo’s weak suit for me so far. I use VS Code for Python — the marimo extension exists, but it’s somewhat janky.

I don’t tend to use it; I run marimo notebooks in the browser.

According to this GitHub issue, I needed the following config for VS Code to find the marimo interpreter and stop it from beachballing forever:

{
  "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
  "marimo.marimoPath": "${workspaceFolder}/.venv/bin/marimo",
  "marimo.debug": true
}

If we have our local Python environment somewhere else, we need to change that too.

It seems incompatible with ruff auto-linting.

4 Markdown

We’ve got rich Markdown support. Nice. It might not behave as expected; Markdown is generated by Python code, so markdown cells aren’t rendered until we execute them, which is different from classic notebooks such as Mathematica or Jupyter. On the other hand, it has upsides, like we can add Python code to our

I could not see it documented, but for markdown to work, the first cell in the notebook should say

import marimo as mo

Symptoms of not doing this: the error NameError('name 'mo' is not defined').

5 Remote access

Marimo runs a web app we can access remotely. We can forward connections manually. Pro tip: it will automatically set up a tunnel if we run it using a VS Code Remote connection.

6 Debugging

Interactive debuggers are supported, though this is only documented in an image on LinkedIn.

pdb.set_trace()
breakpoint()

7 With coding assistants

Let’s say I am using a coding agent. Can we generate a marimo notebook?

First, note that marimo has in-built LLM support. I don’t use this workflow much because I am usually generating a very large codebase, and the notebook context is insufficient. Although maybe I am using it wrong?

So, consider a more general agent. The syntactic constraints of marimo notebooks can work sometimes, but can also be taxing. In principle, generating marimo notebooks from a coding agent is highly efficient because marimo notebooks are pure Python files. In practice there is some friction.

I’ve tested this with Claude. Claude works best when pointed to an example marimo notebook for reference. Show Claude a simple template like that and request new content or edits based on this example, rather than asking it to generate an entire notebook from scratch.

Here is a minimal prompt that might get an assistant generating marimo notebooks OK (it’s worked for me for a while):

  • Marimo notebooks are just .py files with clear internal structure and marked cells.
  • Each cell in marimo begins with Python code, optionally using marimo’s API (import marimo as mo).
  • Variables cannot be redeclared across cells, and each cell forms part of a directed acyclic graph.
  • Test any candidate change for syntactic validity using one of the export commands, e.g. python notebook.py.

Here is a maximal prompt: koaning’s Claude.md file for marimo

It might help to auto-run the notebook with watch mode: marimo edit --watch notebook.py?

8 Exporting/importing

Convert marimo notebooks to other formats (Markdown, Jupyter) using built-in commands:

  • To markdown:

    marimo export md notebook.py -o notebook.md

  • To Jupyter:

    marimo export ipynb notebook.py -o notebook.ipynb

  • Convert markdown back to marimo:

    marimo convert notebook.md > notebook.py.

  • To simply run it — use the fact it’s still a Python script:

    python notebook.py

9 Distributing marimo notebooks

9.1 In Python packages

We already know how to do this via setuptools.

9.2 As environments with self-contained requirements

Nifty! See the following intros

This takes advantage of the PEP 723 inline metadata mechanism, where a code comment at the top of a Python file can list package dependencies (and their versions).

I tried this out by installing marimo using uv:

uv tool install --python=3.12 marimo

Then grabbing one of their example notebooks:

wget 'https://raw.githubusercontent.com/marimo-team/spotlights/main/001-anywidget/tldraw_colorpicker.py'

And running it in a fresh dependency sandbox like this:

marimo run --sandbox tldraw_colorpicker.py

9.3 In the browser

It can run (purely) in the browser without installing Python.

10 Tips

10.1 Dotenv

dotenv is weird in marimo:

dotenv.load_dotenv(dotenv.find_dotenv(usecwd=True))

10.2 Caching some outputs

There is a native cache that caches the output of a cell when we want it to.

10.3 Extra UI widgets

Extra UI widgets? koaning/wigglystuff: A collection of creative AnyWidgets for Python notebook environments.

10.4 Quarto integration

Prototype Quarto integration: marimo-quarto.

10.5 Execution order

Execution order is worth reading about: marimo ensures that cells are executed in a consistent order to maintain reproducibility. If we modify a cell, marimo automatically reruns any dependent cells to keep the notebook’s state consistent. That means cells are not executed in the order we see them on the page, so e.g. we can put boilerplate imports at the end of the notebook. I would not do that because why introduce weirdness?

11 File format

The file format of marimo is clever. It uses Python code to encode Python code. That might not sound revolutionary, but Jupyter used JSON to encode Python code, which has created an ongoing quagmire.

In marimo, there are Jupyter-like cells, but they get their functionality via decorators. They also execute like normal Python code when needed. Here is an example of what a marimo notebook looks like on the inside:

import marimo

__generated_with = "0.9.32"
app = marimo.App(width="medium")

@app.cell
def __():
    import marimo as mo
    return (mo,)

@app.cell
def __():
    print("Hello world")
    return

@app.cell
def __(mo):
    mo.md(
        r"""
        ## Markdown is supported

        You can write in **bold**.
        """
    )
    return

if __name__ == "__main__":
    app.run()

Most notebooks can be exported as plain Python scripts using the marimo export script command.

marimo export script your_notebook.py -o your_notebook_script.py

NB: This doesn’t work if crazy asynchronous stuff is going on.

12 Example notebook

For priming our coding assistant, or general interest, here’s what we expect our notebooks to look like on the inside. They look much nicer when we run them in marimo edit mode.

import marimo

__generated_with = "0.15.2"
app = marimo.App(width="medium")


@app.cell
def _():
    import marimo as mo
    return (mo,)


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
    # example notebook

    this is a markdown cell
    """
    )
    return


@app.cell
def _():
    a = "bar"
    c = "foo"
    d = "baz"
    def example_function(*args):
        return f"return: `args` {args} and local `a` {a}"
    return a, c, d, example_function


@app.cell
def _(c, d, example_function):
    b = example_function(d, c)
    print(b)
    # Expected response
    # return: `args` ('baz', 'foo') and local `a` bar
    return


@app.cell
def _(a, c, d):
    print(a, c, d)
    # Expected response
    # bar foo baz
    return


if __name__ == "__main__":
    app.run()