Jupyter stores your analysis as a linear script of cells that you can run in any order, and that freedom is the source of its most notorious bug: hidden state. Marimo removes it. It treats a notebook as a dependency graph, so changing one cell automatically updates every cell that depends on it. The result is a notebook that behaves like a spreadsheet, stores itself as plain Python, and deploys as an app. Jupyter still wins on ecosystem, language support, and managed infrastructure.
Quick Takeaways
- Execution model: Jupyter runs cells manually in any order. Marimo builds a directed acyclic graph (DAG) from variable definitions and references, then re-runs affected cells automatically.
- File format: Jupyter saves .ipynb JSON with embedded outputs. Marimo saves pure .py files that diff cleanly in Git and run as scripts.
- Ecosystem: Jupyter supports dozens of kernels, JupyterHub, Colab, and a huge extension library. Marimo is Python-first and younger.
- Verdict: Use Marimo for reproducible analysis, interactive tools, and apps. Keep Jupyter for multi-language work, managed platforms, and teams locked into the existing ecosystem.
| Dimension | Jupyter | Marimo |
|---|---|---|
| Execution model | Imperative, manual, any order | Reactive DAG, automatic |
| Hidden state | Common | Eliminated by design |
| File format | JSON (.ipynb) | Python (.py) |
| Git diffs | Noisy | Clean |
| Interactive UI | ipywidgets (callback-based) | mo.ui elements (reactive) |
| App deployment | Voilà, Streamlit (separate tools) | Built in (marimo run) |
| Languages | Python, R, Julia, and more | Python (with SQL and Markdown) |
| Maturity | Very high | Growing |
Why Hidden State Breaks Notebook Reproducibility
Jupyter’s kernel is a long-lived Python process. Each cell mutates that process’s memory, and the notebook file records none of the execution order. Consider this sequence:
- You run cell 1:
threshold = 0.5. - You run cell 2:
df_filtered = df[df["score"] > threshold]. - You scroll back up, change cell 1 to
threshold = 0.8, and run only that cell. - Cell 2 still displays results from
0.5.
Your screen now shows outputs that don’t match your code. Delete a cell and its variable keeps living in memory, so downstream cells keep working until someone restarts the kernel and the notebook crashes.
This is measurable. A 2019 large-scale study of public GitHub notebooks (Pimentel et al.) found that only a minority executed without errors, and a far smaller fraction reproduced the original outputs. Out-of-order execution and missing dependencies drove most failures.
Marimo attacks this at the root. It parses each cell with static analysis, records which variables it defines and which it references, and builds a graph. When you change a cell, marimo re-runs its descendants. If you delete a cell, marimo removes its variables from memory and invalidates dependents.
How Marimo’s Reactive Execution Works
The DAG Rules
Marimo enforces three constraints that make the graph sound:
- One definition per variable. Two cells cannot define the same global name.
- No cycles. Cell A cannot depend on cell B if B depends on A.
- Static references. Marimo infers dependencies from code, not from runtime behavior.
These rules feel restrictive for about a day. They also guarantee that notebook state is a pure function of the code on screen.
A Minimal Reactive Notebook
Install and launch the editor:
pip install marimo # install the package
marimo edit analysis.py # create/open a notebook in the browser
Here is the underlying file. It is ordinary Python:
import marimo
app = marimo.App()
@app.cell
def _():
import marimo as mo
import numpy as np
import pandas as pd
return mo, np, pd
@app.cell
def _(mo):
# A UI element: its .value is tracked by the reactive graph
n_samples = mo.ui.slider(start=100, stop=5000, step=100, value=1000, label="Samples")
n_samples
return (n_samples,)
@app.cell
def _(n_samples, np, pd):
# Re-runs automatically whenever the slider moves
rng = np.random.default_rng(42)
df = pd.DataFrame({"x": rng.normal(size=n_samples.value)})
df.describe()
return (df,)
if __name__ == "__main__":
app.run()
Drag the slider and the DataFrame summary updates with no callback, no observe() handler, and no manual re-run. Because the file ends with app.run(), you can also execute it with python analysis.py.
Controlling Expensive Cells
Auto-execution is risky when a cell trains a model for ten minutes. Marimo provides three controls:
- Lazy runtime mode: marks dependent cells as stale instead of re-running them.
- mo.stop(condition): halts a cell until a condition, such as a button click, is met.
- @mo.cache: memoizes function results based on inputs.
@app.cell
def _(mo):
run_button = mo.ui.run_button(label="Train model")
run_button
return (run_button,)
@app.cell
def _(mo, run_button):
# Block expensive work until the user clicks the button
mo.stop(not run_button.value, mo.md("Click **Train model** to start."))
# ... model training code here ...
return
Jupyter vs Marimo: Detailed Technical Comparison
Version Control and Code Review
An .ipynb file is JSON that bundles source, outputs, execution counts, and metadata. A one-line code change can produce hundreds of changed lines, including base64-encoded images. Teams patch this with tools like nbstripout, Jupytext, or nbdime.
Marimo notebooks are Python files. Diffs show only code changes, and standard linters, formatters, and test runners work without adapters. This single difference often decides adoption for teams that review notebooks in pull requests.
Dependency and Environment Management
| Task | Jupyter | Marimo |
|---|---|---|
| Declare dependencies | External (requirements.txt, conda env) | Optional inline script metadata (PEP 723) |
| Isolated run | Manual venv + kernel registration | marimo edit –sandbox with uv |
| Share a runnable notebook | Ship env file + notebook | Single .py file with embedded dependencies |
Marimo’s sandbox mode installs packages declared in the file into a temporary environment. That makes a notebook closer to a self-contained artifact.
Interactivity and Apps
Jupyter relies on ipywidgets, which use callbacks to push state changes. Turning a notebook into a shareable app typically means adopting Voilà, Streamlit, or Panel, then restructuring code.
Marimo uses one model for exploration and delivery:
marimo edit analysis.py # develop with code visible
marimo run analysis.py # serve as a read-only app, code hidden
Marimo also exports to static HTML (marimo export html) and can run in the browser through WebAssembly, which removes the need for a server for lightweight apps.
Language and Tooling Support
Jupyter’s kernel protocol supports Python, R, Julia, Scala, and many others. Its ecosystem includes JupyterHub for multi-user deployments, nbconvert for export, Papermill for parameterized execution, and integrations in VS Code, Google Colab, Databricks, and SageMaker.
Marimo is Python-centric. It offers SQL cells (querying DataFrames and databases such as DuckDB) and Markdown, but it does not replace an R or Julia workflow.
Performance and Scalability
Marimo’s reactive engine adds graph analysis overhead, which is negligible for typical notebooks. The real scaling concern is the same in both tools: a large DataFrame lives in one process’s memory. For big data, push computation to PySpark, DuckDB, Polars, or a warehouse, and display only aggregated results.
Migrating from Jupyter to Marimo
Marimo ships a converter:
# Convert an existing notebook; review the output for duplicate variable names
marimo convert legacy_analysis.ipynb -o legacy_analysis.py
marimo edit legacy_analysis.py
Expect to fix three things:
- Redefined variables. Notebooks often reuse names like
dfortempacross cells. Rename them or wrap logic in functions. - Mutation across cells. Code like
df["new"] = ...in a different cell than wheredfwas defined breaks the one-definition rule. Create a new variable (df_features = df.assign(...)) instead. - Magic commands. Replace
%matplotlib inlineand similar with regular imports or marimo equivalents.
These fixes tend to improve code quality regardless of the tool.
Real-World Use Cases
Predicting Customer Churn with Interactive Thresholds
A churn model outputs probabilities. The business question is where to set the decision threshold. In Jupyter, an analyst reruns cells repeatedly or builds an ipywidgets callback. In Marimo, a slider feeds the threshold into the metrics cell:
@app.cell
def _(mo):
threshold = mo.ui.slider(0.05, 0.95, step=0.05, value=0.5, label="Churn threshold")
threshold
return (threshold,)
@app.cell
def _(threshold, y_true, y_proba):
from sklearn.metrics import precision_score, recall_score, f1_score
y_pred = (y_proba >= threshold.value).astype(int) # recompute on every slider move
{
"precision": round(precision_score(y_true, y_pred), 3),
"recall": round(recall_score(y_true, y_pred), 3),
"f1": round(f1_score(y_true, y_pred), 3),
}
return
Stakeholders drag the slider and watch precision and recall trade off in real time, then receive the same file as an app via marimo run.
Handling Missing Data in Sensor Streams
Engineers comparing imputation strategies (forward fill, linear interpolation, rolling median) can bind a mo.ui.dropdown to the strategy and plot the repaired series. Because the DAG guarantees consistent state, teammates reproduce the same figure from the same file, with no kernel-restart ritual.
Where Jupyter Remains the Better Choice
- Polyglot teams using R or Julia alongside Python.
- Managed platforms (Databricks, SageMaker, Colab, JupyterHub) built around .ipynb.
- Teaching materials with thousands of existing notebooks.
- Long, stateful exploration where you want to hold a loaded model in memory and probe it manually without triggering re-execution.
Are Reactive Notebooks the Future?
Reactive execution solves a real, documented problem, and it has precedent: spreadsheets, Observable, and Pluto.jl all use dependency-driven evaluation. Marimo brings that model to Python with a file format that fits modern software practice.
Adoption depends on factors beyond technical merit. Jupyter’s network effects are enormous: cloud vendors, universities, and thousands of tutorials assume .ipynb. A realistic outlook is coexistence. Jupyter stays the default for exploration inside established platforms, while reactive notebooks gain ground where reproducibility, code review, and app delivery matter.
A practical test: if you have ever sent a colleague a notebook that “worked on my machine,” run a small project in Marimo this week and compare the experience.
Decision Framework
| If your priority is… | Choose |
|---|---|
| Reproducibility and no hidden state | Marimo |
| Clean Git history and PR reviews | Marimo |
| Sharing results as interactive apps | Marimo |
| R, Julia, or multi-language kernels | Jupyter |
| Enterprise platform integration | Jupyter |
| Maximum extension and community support | Jupyter |
FAQ
Is Marimo better than Jupyter?
Marimo is better for reproducibility, version control, and interactive apps because it uses a reactive DAG and plain .py files. Jupyter is better for multi-language support and platform integration. The right choice depends on your workflow.
Can Marimo open Jupyter notebooks?
Not directly, but it converts them. Run marimo convert notebook.ipynb -o notebook.py, then fix any redefined variables, since Marimo allows only one definition per name.
Does Marimo replace Jupyter completely?
No. Marimo supports Python (plus SQL and Markdown) and lacks the kernel ecosystem and managed-platform integrations that Jupyter has. Many teams use both.
How does Marimo prevent hidden state?
It analyzes each cell’s variable definitions and references to build a dependency graph. Changing or deleting a cell updates or invalidates every dependent cell, so displayed outputs always match the code.




