DEV Community

Cover image for Marimo vs Jupyter: Reactive Notebooks for Data Work
Muhammad Saud
Muhammad Saud

Posted on Originally published at logicstack.org

Marimo vs Jupyter: Reactive Notebooks for Data Work

Marimo is an open-source Python notebook built around reactive execution: change one cell and every cell that depends on its variables re-runs automatically, like formulas recalculating in a spreadsheet. Notebooks are stored as plain .py files rather than Jupyter's JSON, so they version cleanly in git, run as scripts, and deploy as web apps. It removes Jupyter's hidden-state bugs by construction.

Key takeaways

  • Marimo re-runs dependent cells automatically through a statically analysed dataflow graph. You cannot run cells out of order, so code and outputs cannot drift apart.

  • Notebooks are plain Python files, which means clean git diffs, ruff and black formatting, pytest, and running notebooks with python notebook.py. I converted a two-cell .ipynb with marimo convert and executed the result as a script.

  • One hard rule trips up Jupyter users: each variable may be defined in exactly one cell. Redefining df in a scratch cell raises a MultipleDefinitionError.

  • Jupyter still wins on ecosystem: multi-language kernels, Colab's free GPUs, papermill for parameterised runs, and a decade of courses and Stack Overflow answers.

  • Use marimo for new analysis you intend to version, share, or deploy as an app. Stay on Jupyter for courses, GPU training on Colab, and non-Python kernels.

Why do Jupyter notebooks break when you re-run them?

Jupyter began life as a scratchpad. It descends from IPython, a terminal REPL started as an academic project in 2001, and the notebook itself shipped in 2014 as "IPython in the browser". Like a REPL, Jupyter lets you execute cells one at a time, in any order, each mutating a shared workspace in memory. That was fine for sketching, but notebooks grew up: research, experimentation, model training, and data engineering now run through them, and a scratchpad was never designed for that.

The core problem is hidden state. Run a cell, delete it, and its variables stay in memory, silently referenced by the cells that remain. Run cells out of order and the outputs on screen describe a program that no longer exists. This is not a theoretical complaint. A study of 10 million Jupyter notebooks cited in marimo's official documentation found that 36% of them were not reproducible (marimo docs FAQ, citing a JetBrains engineering study). The marimo team separately cites research finding over 75% of GitHub notebooks do not even run (marimo.io comparison page).

Then there is the file format. Jupyter notebooks are JSON with embedded outputs, execution counts, and metadata. Re-run one cell and the .ipynb diffs, because the execution metadata changes even when your code did not. Reviewing a colleague's notebook in a pull request means scrolling past base64-encoded images and widget state to find the three lines that changed. Tools like jupytext and nbstripout exist to paper over this, which tells you how real the pain is.

How does reactive execution actually work?

Marimo replaces Jupyter's run-whatever-you-click model with a dataflow graph. It reads each cell once and records which global names the cell defines and which it reads, using static analysis. When a cell runs, marimo runs every cell that reads one of its defined names, in dependency order. Edit a data-loading cell and the cleaning, analysis, and plotting cells downstream re-run by themselves. Delete a cell and marimo scrubs its variables from program memory, so nothing downstream can secretly depend on a ghost.

Two details matter in practice. First, the analysis happens by reading your code, not running or tracing it, so there is zero runtime overhead on your actual computation (marimo docs FAQ). Second, automatic execution is configurable. If re-running everything would be expensive, you can switch the runtime to mark affected cells as stale instead of running them, disable individual cells, stop execution conditionally with mo.stop, or cache expensive steps with mo.cache and mo.persistent_cache.

The closest analogy is a spreadsheet, and it is a deliberate one: marimo's inspirations include Observable and the reactive notebook Pluto.jl. The payoff is that "restart and run all" stops being a ritual you perform before sharing a notebook, because the notebook was already in a consistent state the whole time.

What does a marimo notebook look like on disk?

This is where the difference becomes concrete. I installed marimo 0.25.2 (current release as of 20 October 2026, requires Python 3.10 or newer) and converted a real two-cell Jupyter notebook with one command:

marimo convert notebook.ipynb > notebook.py
Enter fullscreen mode Exit fullscreen mode

The output was 23 lines of ordinary Python:

import marimo

__generated_with = "0.25.2"
app = marimo.App()

@app.cell
def _():
    import pandas as pd
    df = pd.read_csv('sales.csv')
    return (df,)

@app.cell
def _(df):
    threshold = 500
    df[df['revenue'] > threshold].shape[0]
    return

if __name__ == "__main__":
    app.run()
Enter fullscreen mode Exit fullscreen mode

Each cell is a function decorated with @app.cell. Variables a cell defines are returned explicitly; variables it reads arrive as function arguments. Execution order is determined by those references, not by where cells sit on the page. And because it is just Python, the notebook runs as a script: I executed the converted file with python notebook.py and it ran end to end with no notebook server involved.

That file format is the whole git story. A diff of a marimo notebook shows code changes and nothing else: no execution counts, no base64 images, no widget metadata. Standard tooling applies directly. You can run ruff or black on the file, test cells with pytest, import functions from one notebook into another, and serve the notebook as an interactive web app with marimo run notebook.py, which hides the code and exposes the outputs and UI elements.

How do you rebuild a Jupyter analysis in marimo?

Start with pip install marimo (or pip install "marimo[recommended]" for SQL cells, AI completion, and extras), then marimo edit to open the browser editor. Existing notebooks come across with marimo convert, and the editor has a web importer for pasting .ipynb content directly.

The first thing that will bite you is the one-definition rule. In Jupyter you redefine df in scratch cells all afternoon; in marimo that is an error. I wrote a notebook defining x in two cells and running it produced:

critical[multiple-definitions]: Variable 'x' is defined in multiple cells
hint: Variables must be unique across cells. Alternatively, they can be
private with an underscore prefix (i.e. `_x`.)
marimo._ast.errors.MultipleDefinitionError: This app can't be run because
it has multiple definitions of the name x
Enter fullscreen mode Exit fullscreen mode

The fix is to give scratch variables underscore-prefixed names like _df, which are treated as cell-private, or to restructure so each name has one home. It feels strict for the first hour and then starts to feel like the discipline you always meant to have.

Interactivity is where marimo pulls ahead of Jupyter without extra frameworks. A slider that filters a dataframe takes two cells and zero callbacks:

import marimo as mo

@app.cell
def _():
    import marimo as mo
    threshold = mo.ui.slider(0, 1000, value=500)
    threshold
    return (threshold,)

@app.cell
def _(df, threshold):
    filtered = df[df["revenue"] > threshold.value]
    filtered
    return (filtered,)
Enter fullscreen mode Exit fullscreen mode

Assign the element to a global, show it as the cell's last expression, and read .value in a downstream cell. Dragging the slider re-runs the filter cell automatically. In Jupyter the equivalent needs ipywidgets, observers, and manual re-execution. Marimo also ships reactive matplotlib selections, editable matrix inputs, SQL cells that query dataframes and databases, and built-in package management that can pin a notebook's dependencies inside the file itself.

Where does Jupyter still win?

Marimo is the better notebook design, but Jupyter is the incumbent with a decade-long head start, and incumbency counts.

First, languages. Marimo is Python-only. If your team mixes R, Julia, or Scala in notebooks, Jupyter's kernel architecture has no substitute. Second, hosted GPUs. Google Colab remains the fastest way to get a free GPU under a notebook, and its sharing model is built for classrooms. Entire courses are taught on Colab; no department is migrating its curriculum this semester. Third, the ecosystem: JupyterLab's extension marketplace, papermill for parameterised notebook runs in pipelines, nbconvert, and Voilà for dashboards all have years of production use behind them. Marimo's answers (run as script, run as app, pytest) are cleaner, but younger.

There are smaller frictions too. The one-definition rule will annoy anyone porting a sprawling exploratory notebook before they restructure it. Some teams rely on notebook extensions with no marimo equivalent. And while marimo is AI-native by design (plain-text files that agents like Claude Code can read and edit, plus a marimo check linter built for agent feedback loops), the agent workflows around Jupyter are simply more battle-tested because more people use them.

None of this is a reason to dismiss marimo. It is a reason to be honest about switching costs, which are real but one-time.

Should you switch from Jupyter to marimo?

Use this decision rule. Reach for marimo when you are starting analysis work that will be versioned in git, shared with colleagues, or deployed as an internal tool: that is where reproducibility, clean diffs, and one-command app deployment pay off immediately. Convert existing notebooks with marimo convert when they become something you maintain rather than something you ran once. Stay on Jupyter when the course, the team, or the GPU hours live there, when you need non-Python kernels, or when papermill already orchestrates your notebook pipelines.

The broader point is the one the marimo team keeps making: notebooks stopped being scratchpads years ago, and the tooling should catch up to how we actually use them. Whether marimo becomes the default or simply pressures Jupyter to adopt reactive execution and cleaner file formats, the direction is the same. Try it on your next analysis: pip install marimo && marimo tutorial intro takes about a minute, and the reactive model clicks the first time a slider drag re-runs your whole pipeline.


Originally published on LogicStack. Run the code examples right in your browser with our free interactive Python and SQL playgrounds — no setup, no sign-up.

Top comments (0)