Skip to content
Published on

The New Python Notebook Stack in 2026 — Marimo, Quarto, and the Post-Jupyter Data Workflow (Observable, Pluto, Polars, DuckDB)

Authors

Prologue — Jupyter's hidden-state problem

Even in 2026, the same scene repeats weekly. A data scientist hands a notebook to a colleague, the colleague hits "Run All", and the fifth cell explodes. A variable is undefined. But on the sender's machine, it ran fine.

The cause is almost always the same — hidden state. If you execute cells in arbitrary order without ever running top-to-bottom, the notebook text and the kernel's memory drift apart. The sender's kernel still has df alive, but the notebook file no longer contains the cell that defined it.

This isn't a Jupyter bug. It's a design decision. Jupyter treats out-of-order execution as a feature — that's the essence of a REPL. But that freedom becomes a liability the moment two people collaborate, the moment you ship to CI, the moment six months pass.

"Notebooks are heaven for one person and hell for two or more."

Between 2024 and 2026, serious efforts emerged to pay down this debt with a different design. Marimo statically analyzes the dataflow between cells and re-runs cells based on dependencies — not cell order, but data dependency, is truth. Quarto takes notebooks as input and publishes them as HTML, PDF, websites, books, paper supplements — multi-language, multi-format. Observable Framework builds JS-native notebooks into static sites. Below all of them, the compute layer is moving from Pandas to Polars and DuckDB.

This post is a deep look at that stack — what Jupyter got wrong, how Marimo solved it, how Quarto publishes, and when you should still reach for Jupyter.


1. The four debts of Jupyter

Jupyter (more precisely: IPython kernel + ipynb JSON + Notebook/Lab UI) has been the standard data-science environment since 2014. It got many things right — putting outputs next to code, mixing inline visualizations and Markdown and LaTeX into "executable documents." But twelve years in, four debts have accumulated.

Debt 1 — Hidden state

Even if you execute cells in the order In[14], In[3], In[27], the notebook file does not record that order. There's no guarantee that "Run All" produces the same result later. Truth lives in the kernel running in memory, not in the notebook file, and that kernel evaporates the moment it shuts down.

Debt 2 — Not diffable (.ipynb is JSON)

.ipynb packs code, Markdown, images, outputs, and execution counters into one JSON file. When a PR reviewer opens the diff on GitHub, they see an endless single line of base64-encoded PNG. The fact that workarounds like nbstripout and jupytext exist is itself the signal — the format is fundamentally incompatible with git.

Debt 3 — Not reproducible

Combine the first two debts and reproducibility falls apart. "I ran this notebook" does not mean "in this environment, in this order, on this data." Open a paper-supplement notebook six months later and the dependencies are broken, outputs don't match code.

Debt 4 — One-directional UI

Jupyter assumes a "top-to-bottom" linear model. There's no graph view showing data dependencies — to know where the output of this cell flows, or which cell depends on which, you have to track it mentally. Past fifty cells, this becomes practically impossible.

These four debts reinforce each other. Hidden state means no reproducibility, no reproducibility means diffs are meaningless, meaningless diffs mean no collaboration.


2. Marimo — redesigning the notebook

Marimo is an open-source Python notebook that emerged in late 2023 and, as of mid-2026, sits in the 0.10.x series. Its starting point is simple — "What changes if we redesign the notebook from scratch?"

Three core design decisions:

Decision 1 — Store as .py files

Marimo notebooks are not .ipynb. They are plain Python files. Each cell is a decorated function.

import marimo as mo

app = mo.App()


@app.cell
def __():
    import polars as pl
    df = pl.read_csv("sales.csv")
    return (df,)


@app.cell
def __(df):
    summary = df.group_by("region").agg(pl.col("revenue").sum())
    return (summary,)


@app.cell
def __(summary):
    summary
    return ()


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

Two things follow. First, the file also runs as a Python script — just python notebook.py. Second, git diffs are meaningful. Rename a variable and you see the rename in the diff. There is no base64-encoded PNG anywhere.

Decision 2 — Dataflow graph, not cell order

Marimo statically analyzes the notebook to find which variables each cell defines and which it references. This becomes a directed acyclic graph (DAG). When you run a cell, Marimo automatically re-runs every cell that depends on it — the same model as an Excel spreadsheet.

The result: hidden state is impossible. Reassign a variable and every cell using it updates instantly. Delete a cell and any cell using its variables immediately raises "not defined." The notebook file and the kernel state stay in sync by construction.

Decision 3 — Notebook-as-app mode

Marimo can launch the same file in two modes:

  • Edit mode — a normal notebook for writing and running cells.
  • Run mode — launch with marimo run notebook.py and cells disappear, leaving only UI components (sliders, dropdowns, tables) as an interactive app.

UI components live in the mo.ui module.

@app.cell
def __(mo):
    region = mo.ui.dropdown(
        options=["US", "EU", "APAC"],
        value="US",
        label="Region",
    )
    region
    return (region,)


@app.cell
def __(df, region):
    filtered = df.filter(pl.col("region") == region.value)
    filtered
    return (filtered,)

Change the dropdown and every cell consuming the value re-runs automatically. The notebook itself becomes an app — no Streamlit or Gradio needed.

Additional wins

  • WASM deployment — Marimo exports notebooks as static HTML via Pyodide. No server. Users interact directly in the browser.
  • AI assist — Per-cell AI completion (model-agnostic) is built in.
  • Type checking — Because it's a .py file, mypy and pyright just work.
  • Testing — Import notebook cells into pytest and unit-test them.
  • Pure-function cells — Each cell is a function with explicit inputs and outputs, which makes unit testing natural.

3. Jupyter vs Marimo — same task, different code

Here is the same workflow (load data, filter, visualize) in both.

Jupyter cells

# Cell 1
import pandas as pd
df = pd.read_csv("sales.csv")
df.head()

# Cell 2
df_us = df[df["region"] == "US"]
df_us.shape

# Cell 3
import matplotlib.pyplot as plt
df_us.groupby("month")["revenue"].sum().plot(kind="bar")
plt.show()

# Cell 4 — thirty minutes later, forgot the original df
df = pd.read_csv("sales_v2.csv")  # df_us still points at the old data!

Run Cell 4, look at the chart in Cell 3 — it won't update, because df_us captured the old df. Memory state and code state are out of sync. Hidden state.

Marimo cells

@app.cell
def __():
    import polars as pl
    df = pl.read_csv("sales.csv")
    df
    return (df, pl)


@app.cell
def __(df, pl):
    df_us = df.filter(pl.col("region") == "US")
    df_us
    return (df_us,)


@app.cell
def __(df_us):
    chart = df_us.group_by("month").agg(pl.col("revenue").sum())
    chart
    return (chart,)

Change the first cell to load sales_v2.csv and Marimo automatically re-runs the second and third cells. The chart updates immediately. Memory state and code state stay synchronized by construction.

Also: Marimo forbids redefining the same variable in two cells. It raises "this variable is already defined elsewhere" and forces you to pick one. This is jarring at first — and hidden state vanishes forever.


4. Quarto — turning notebooks into publishable documents

If Marimo is "redesign the notebook," Quarto is "publish the notebook." Quarto is the successor to RStudio's RMarkdown, hit 1.0 in 2022, and is at 1.5+ as of 2026. It handles multiple languages (Python, R, Julia, Observable JS) and multiple formats (HTML, PDF, EPUB, Word, websites, books) from one tool.

A Quarto document

Quarto uses .qmd extension, with YAML frontmatter plus Markdown plus code fences.

---
title: "2025 Sales Analysis"
author: "Data Team"
date: "2026-05-14"
format:
  html:
    code-fold: true
    toc: true
  pdf:
    documentclass: article
execute:
  echo: true
  warning: false
---

## Load the data

We analyze this quarter's revenue.

\`\`\`{python}
import polars as pl
df = pl.read_csv("sales.csv")
df.head()
\`\`\`

## Regional totals

\`\`\`{python}
df.group_by("region").agg(pl.col("revenue").sum())
\`\`\`

(Backtick fences are escaped above so MDX doesn't get confused. In a real .qmd you just use three backticks.)

Build with quarto render report.qmd --to html for interactive HTML, or --to pdf for a LaTeX-quality PDF. The code runs, and outputs are embedded in the document.

Output formats Quarto supports

  • HTML — search, themes, interactive widgets, Mermaid diagrams, MathJax.
  • PDF (LaTeX) — academic-paper typography. The book class also generates books.
  • Websites — bundle multiple .qmd files into a static site (Jekyll/Hugo-style).
  • Books — multi-chapter PDF/HTML books. R for Data Science is built with Quarto.
  • Reveal.js slides — put executed code outputs into slides.
  • Word, EPUB — for publisher workflows.

Multi-language — Python, R, Julia, Observable JS in one file

Quarto reads the code-fence language identifier and dispatches to the right kernel. python fences run on the Jupyter kernel, r on knitr, julia on IJulia, {ojs} on the Observable JS runtime. You can preprocess data in Python, fit a model in R, and render an interactive chart in Observable JS — all in the same document.

Quarto + Marimo, Quarto + Jupyter

Quarto accepts not just .qmd but also Jupyter notebooks (.ipynb) and Marimo notebooks as input. Quarto is notebook-format-agnostic. The workflow "author in Marimo, publish with Quarto" is natural.

Academic papers and supplements

Quarto ships official templates for journals like Journal of Statistical Software — APA, IEEE, Nature, Elsevier formats are one frontmatter line. Citations pull straight from BibTeX or Zotero and format via CSL. The paper and its analysis code live in the same file — the endgame of reproducibility.


5. Observable Framework — the evolution of JS-native notebooks

Observable, started around 2018 by Mike Bostock (creator of d3.js), is a JS-native notebook. Cells form a reactive graph (the inspiration for Marimo) and visualization is a first-class citizen. The catch was lock-in to Observable.com hosting.

Observable Framework, announced in 2024, brought the model to a static-site builder. Embed JS cells in .md files, run observable build, and you get static HTML/CSS/JS. Host on Vercel, Netlify, GitHub Pages — wherever.

Where Observable Framework shines:

  • Data dashboards — internal analytics sites, KPI tracking, operational dashboards.
  • Interactive data storytelling — d3 visualizations, charts that respond to user input, data journalism.
  • Build-time data loaders — refresh data via Python/R/shell at build time, freeze the result as static assets.

If Marimo answers "I'm a Python user and I want interactivity," Observable Framework answers "I'm JS-and-visualization first and I want publishable artifacts." They aren't competitors — they serve different markets.


6. Pluto.jl — Julia's reactive notebook, and the original

Pluto.jl is a reactive notebook for Julia, and Marimo's direct inspiration. It launched in 2020 and was the first to bring the "cell-dataflow graph leads to auto-rerun" model to the notebook world.

Core principles Pluto introduced:

  • The notebook file is the single source of truth — not the in-memory kernel; the file defines state.
  • Cell order is irrelevant — wherever a cell lives in the file, the dependency graph determines execution order.
  • The same variable cannot be redefined across cells — that would make the graph ambiguous.
  • Stored as .jl files — also runs as a plain Julia script.

Marimo brought this model to Python. Pluto has meanwhile gone deeper with Julia's data-science stack (SciML, Plots.jl, DataFrames.jl). If you use Julia, Pluto is the default choice.


7. The new compute layer — Polars and DuckDB

It's not just the notebook UX that changed. The compute layer underneath has too.

Polars — Rust-based DataFrame

Polars is a DataFrame library written in Rust, with Apache Arrow as its native memory format. Compared to Pandas:

  • Fast — multithreaded by default, columnar memory layout, query optimizer.
  • Lazy evaluation — build a query, then .collect() to execute all at once (the Spark/SQL model).
  • Consistent API — none of Pandas' accumulated inconsistencies (apply vs applymap, index hell, etc.).
  • Small memory footprint — Arrow zero-copy so multiple tools share the same data.

In a heavy notebook, moving to Polars is commonly 5x–30x faster.

DuckDB — in-process OLAP database

DuckDB is the OLAP (analytic) cousin of SQLite. Embedded, columnar, optimized for single-node analytics. It queries files, Parquet, CSV, JSON, and Arrow directly.

Where DuckDB shines in a notebook:

import duckdb
duckdb.sql("""
    SELECT region, SUM(revenue)
    FROM 'sales/*.parquet'
    WHERE date >= '2026-01-01'
    GROUP BY region
""").to_df()

Query 100 GB of Parquet directly from a notebook — no Spark cluster needed. Polars and DuckDB exchange data via Arrow with zero copies, so mixing them in the same pipeline has no overhead.

When this compute layer sits below Marimo and Quarto, the data volume a single notebook can handle moves from single-digit GB to three-digit GB.


8. Comparison matrix

ItemJupyterMarimoQuartoObservable FrameworkPluto.jl
LanguagePython (many kernels)PythonMulti-languageJS-centricJulia
File format.ipynb (JSON).py.qmd.md.jl
Reactive executionNoYes (DAG)No (publishing tool)YesYes (original)
Git diffPoorGoodGoodGoodGood
Notebook-as-appSeparate (Voila, Panel)Built-inSeparateStatic-site buildPartial
Static exportHTML (no rerun)HTML/WASMHTML, PDF, sites, booksStatic sitesHTML
Learning curveLowLow to mediumMediumMedium (needs JS)Low
Ecosystem sizeMassiveGrowingLargeVisualization-focusedMid (Julia)
Primary useEDA, teaching, researchEDA, internal tools, appsReports, papers, books, blogsDashboards, data journalismJulia scientific computing
LicenseBSD-3Apache-2.0MITISCMIT

9. Real workflow scenarios

Scenario A · Daily EDA for a data scientist

  • Before: Jupyter Lab plus Pandas. Re-run cells, fall into hidden state. Send results to a colleague as PNG screenshots.
  • After: Marimo plus Polars plus DuckDB. Notebook is .py, so it lands as a PR. The colleague runs marimo run and reproduces the result immediately. Drop a slider into a cell to create a "select region" UI so non-data colleagues can also poke at it.

Scenario B · Quarterly report for executives

  • Before: Analyze in Jupyter, screenshot into PowerPoint. Next quarter, re-screenshot.
  • After: Quarto website. Refresh data each quarter and quarto render. Output ships as static HTML on the internal intranet. Search, themes, and print-ready PDF all in one build.

Scenario C · Academic paper plus supplement

  • Before: LaTeX for the paper, separate .ipynb for analysis. Reviewer comments arrive six months later and nobody can find which notebook produced which figure.
  • After: Quarto plus Marimo. Paper body and analysis code in the same .qmd. quarto render --to pdf for LaTeX output. Build the same file with --to html for the supplement. A reproducible paper.

Scenario D · Internal data tool

  • Before: Build a separate app in Streamlit/Gradio. Data scientist works in the notebook, engineer ports it to an app — written twice.
  • After: Marimo notebook-as-app. Drop mo.ui components into the notebook and serve with marimo run. No engineering handoff. One file is both notebook and app.

Scenario E · Data journalism, interactive stories

  • Observable Framework. d3.js visualizations, charts that respond to user input, build-time data loaders (preprocess in Python, freeze as static JSON), shipped as a static site.

Scenario F · Julia scientific computing

  • Pluto.jl. The Julia ecosystem (SciML, DifferentialEquations.jl) runs deep, and the reactive model fits simulations and parameter sweeps well.

10. When to still pick Jupyter

Powerful as Marimo and Quarto are, Jupyter is still the right call in places.

Stay on Jupyter when

  • You depend on specialty kernels — Spark, PyTorch Distributed, Wolfram Language. Jupyter's kernel protocol is the standard there.
  • You already run JupyterHub or Binder — Universities and research labs cannot rip these out overnight.
  • One-off scratch work — For a five-line experiment, Jupyter's lower setup cost wins.
  • Teaching — When the lecture's whole point is the cell-order, In[N]/Out[N] mental model. (Though Marimo-taught courses are growing in 2026.)
  • VS Code, Cursor integrated notebook UI.ipynb is deeply wired into the IDE-first workflow.

Signals to move to Marimo

  • You treat notebooks as collaboration assets (PRs, reviews).
  • "Run All" breaks frequently.
  • You want to ship notebooks as internal tools.
  • Non-data colleagues need to use the notebook.

Signals to move to Quarto

  • You publish results externally on a regular cadence (blog, report, paper).
  • The same content must ship in multiple formats (HTML and PDF).
  • You make books or multi-chapter documents.
  • You want to combine Python, R, and JS analysis in one document.

11. Migration — how to move

Jupyter to Marimo

Marimo provides marimo convert notebook.ipynb for automatic conversion. It will not be perfectly clean — patterns Jupyter permitted (redefining a variable across cells) error in Marimo. Budget thirty to sixty minutes of hand-tuning after the first conversion.

Recommendation: start new notebooks in Marimo, leave existing .ipynb alone. Do not migrate everything at once.

Jupyter to Quarto

Quarto accepts .ipynb directly as input — no conversion needed. Just run quarto render notebook.ipynb. The value-add is the expanded output formats.

Marimo plus Quarto together

The strongest combo. Author and explore interactively in Marimo, then publish with Quarto. Since both speak .py and .ipynb, the pipeline is clean.


12. Anti-patterns

The new stack has its own traps.

Anti-pattern 1 — Using Marimo as a Jupyter drop-in

Marimo's real value is reactivity, .py storage, and notebook-as-app. Skip those and you pay the learning curve for zero gain. Use UI components, lean on reactive dependencies.

Anti-pattern 2 — Embedding huge analysis in a Quarto doc

Quarto build times explode. Split heavy preprocessing into a separate script, and let Quarto only load the resulting data (Parquet or CSV). Or cache with freeze: true.

Anti-pattern 3 — Keeping notebooks as notebooks forever

Even with Marimo and Quarto, putting all code into notebooks is a trap. Stable data pipelines belong in regular .py modules. Notebooks are for exploration, documentation, and interface. Notebooks are not the destination — they are an interface.

Anti-pattern 4 — Refusing to try Polars/DuckDB and sticking with Pandas

Past a few GB, Pandas falls apart in both memory and speed. The cost of moving to Polars is under a week. That week halves your wait time for the next year.

Anti-pattern 5 — Hacking around Marimo's redefinition rule

When the rule feels annoying, do not hack around it with globals(). That rule is the core mechanism stopping hidden state. Rename the variable, or merge the cells.


Epilogue — the next decade of notebooks

Jupyter was the standard data-science workflow from 2014 to 2024. In 2026, that standard isn't broken — it is splitting.

  • Exploration, internal tools, apps → Marimo (or Pluto for Julia).
  • Publishing, papers, books, multi-format → Quarto.
  • JS-native visualization, static sites → Observable Framework.
  • Fast one-off scratch, multi-language kernels → still Jupyter.

And below them all, Polars, DuckDB, Arrow form the new compute layer.

The point: a notebook is no longer just "a tool to run code once." A notebook can simultaneously be a reproducible collaboration asset (Marimo's .py), a publishable document (Quarto), an interactive app (Marimo run mode), and a data story (Observable) — all four interfaces from one file.

The notebook is not the destination — it is an interface. A good interface opens differently for every reader: as code for the data scientist, as an app for the colleague, as a PDF for the executive, as a static site for the internet.

Checklist — should you move?

  • Do you review notebooks via PR? (If yes, move to Marimo.)
  • Has "Run All" ever broken? (If yes, move to Marimo.)
  • Do you publish results externally? (If yes, add Quarto.)
  • Do non-data colleagues need to use the result? (If yes, Marimo notebook-as-app.)
  • Is your data past a few GB? (If yes, move to Polars and DuckDB.)
  • Do you use the Julia ecosystem? (If yes, Pluto.jl.)
  • Are you JS-and-visualization first? (If yes, Observable Framework.)

Anti-pattern summary

  • Using Marimo as a Jupyter drop-in (skip reactivity and app mode, get no gain).
  • Stuffing heavy preprocessing into Quarto (split into a separate script).
  • Keeping notebooks as notebooks forever (stabilize, then promote to modules).
  • Refusing to try Polars/DuckDB and sticking with Pandas (falls apart past a few GB).
  • Hacking around Marimo's redefinition rule (hidden state comes right back).

Next posts

  • The new data-pipeline standard — Dagster, Prefect, Airflow compared from a 2026 lens. If notebooks are exploration, pipelines are production.
  • Polars in depth — the expression model, lazy evaluation, and the Streaming engine for Pandas migrants.
  • Querying 100 GB of Parquet on a single node with DuckDB — analytic workflows without Spark.

References