Marimo: Reactive Python Notebooks That Run as Apps
PROGRAMMING LANGUAGES April 13, 2026, 11:30 p.m.

Marimo: Reactive Python Notebooks That Run as Apps

Imagine a notebook that feels like a web app—reactive, interactive, and instantly shareable—without leaving the comfort of plain Python. That’s exactly what Marimo promises. It blends the familiar notebook workflow with a reactive UI model, letting you build dashboards, visual editors, and data‑exploration tools in seconds. In this article we’ll unpack how Marimo works, walk through two hands‑on projects, and share pro tips to keep your notebooks tidy and production‑ready.

What Is Marimo?

Marimo is an open‑source Python library that turns Jupyter‑style notebooks into reactive applications. Each cell declares its dependencies, and Marimo automatically re‑executes only the affected cells when inputs change. This model mirrors modern front‑end frameworks like React, but you stay in pure Python—no JavaScript required.

Under the hood Marimo uses a lightweight runtime to track a directed acyclic graph (DAG) of cell relationships. When a widget updates, the runtime walks the graph, re‑runs the minimal set of cells, and updates the UI instantly. The result feels like a live app, yet you retain the linear, narrative style of a notebook.

Core Concepts

  • Cells as components: Every code block is a component that can produce output, widgets, or side effects.
  • Dependency tracking: Marimo parses variable references to build a dependency graph automatically.
  • Reactive UI primitives: Built‑in widgets (sliders, text boxes, file uploaders) emit events that trigger re‑execution.
  • App mode: Run marimo run my_notebook.py to serve the notebook as a standalone web app.

Getting Started

Installation is a single pip command, and you’re ready to write your first reactive notebook. The library works on Linux, macOS, and Windows, and it plays nicely with popular data‑science stacks like pandas, matplotlib, and plotly.

  1. Open a terminal and run pip install marimo.
  2. Create a new file, for example dashboard.py, and start it with import marimo as mo.
  3. Launch the notebook locally with marimo edit dashboard.py or serve it as an app with marimo run dashboard.py.

That’s it—no extra configuration, no Dockerfile, no front‑end build steps. Let’s see a minimal example that showcases the reactive loop.

First Notebook: A Simple Counter

The classic “Hello, World!” of reactive apps is a counter that increments when you click a button. Below is a complete Marimo notebook that you can copy‑paste into counter.py and run.

import marimo

# Declare a widget that emits an integer value.
counter = mo.ui.number(value=0, label="Current count", step=1)

# Define a cell that depends on the widget.
@mo.cell
def display_counter(counter=counter):
    # The function is re‑executed whenever `counter` changes.
    return f"The count is **{counter}**"

# Render the UI: the widget on top, the output below.
mo.ui.run(counter, display_counter)

When you run marimo run counter.py, a small web page appears with a numeric input and a live text output. Changing the number instantly updates the displayed count—no manual refresh needed.

Reactive Paradigm in Action

Now that you’ve seen the basics, let’s dive into real‑world scenarios where Marimo shines. The key advantage is that you can compose complex visualizations and data pipelines without writing any explicit callbacks or state‑management code.

Example 1: Live Data Dashboard

Suppose you need a dashboard that monitors cryptocurrency prices and lets users filter by coin and time window. With Marimo, the entire pipeline—from data fetch to chart rendering—updates automatically as the user tweaks the controls.

import marimo as mo
import pandas as pd
import httpx
import plotly.express as px

# 1️⃣ Widgets for user input.
coin = mo.ui.dropdown(options=["BTC", "ETH", "LTC"], label="Coin")
days = mo.ui.slider(min=1, max=30, step=1, label="Days back")

# 2️⃣ Cell that fetches data based on widget values.
@mo.cell
def fetch_data(coin=coin, days=days):
    url = f"https://api.coingecko.com/api/v3/coins/{coin.lower()}/market_chart"
    params = {"vs_currency": "usd", "days": days}
    resp = httpx.get(url, params=params).json()
    prices = resp["prices"]
    df = pd.DataFrame(prices, columns=["timestamp", "price"])
    df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
    return df

# 3️⃣ Cell that builds the plot.
@mo.cell
def price_chart(df=fetch_data):
    fig = px.line(df, x="timestamp", y="price", title=f"{coin.value} Price")
    return mo.ui.plotly(fig)

# 4️⃣ Layout the UI.
mo.ui.run(coin, days, price_chart)

When a user selects a different coin or moves the slider, the fetch_data cell runs again, pulling fresh data from the API. The price_chart cell then re‑renders the Plotly line chart with the new dataframe. No explicit event listeners—Marimo’s dependency graph does the heavy lifting.

Example 2: Interactive Image Annotator

Another common need is an image annotation tool for machine‑learning pipelines. Below is a compact Marimo notebook that lets you upload an image, draw bounding boxes with a simple UI, and export the coordinates as JSON.

import marimo as mo
import json
from PIL import Image
import matplotlib.pyplot as plt

# 1️⃣ File uploader widget.
uploader = mo.ui.file(label="Upload Image", accept="image/*")

# 2️⃣ Cell that loads the image.
@mo.cell
def load_image(file=uploader):
    if file is None:
        return None
    img = Image.open(file.path)
    return img

# 3️⃣ Cell that displays the image and captures clicks.
@mo.cell
def annotate(img=load_image):
    if img is None:
        return "Upload an image to start annotating."

    fig, ax = plt.subplots()
    ax.imshow(img)
    ax.set_title("Click two points to define a bounding box")
    points = []

    def onclick(event):
        if event.inaxes != ax:
            return
        points.append((event.xdata, event.ydata))
        if len(points) == 2:
            # Draw rectangle.
            x0, y0 = points[0]
            x1, y1 = points[1]
            rect = plt.Rectangle(
                (min(x0, x1), min(y0, y1)),
                abs(x1 - x0),
                abs(y1 - y0),
                edgecolor="red",
                facecolor="none",
                linewidth=2,
            )
            ax.add_patch(rect)
            plt.draw()
            # Store result for the next cell.
            annotate.box = {"x0": min(x0, x1), "y0": min(y0, y1),
                            "x1": max(x0, x1), "y1": max(y0, y1)}
            points.clear()

    cid = fig.canvas.mpl_connect("button_press_event", onclick)
    mo.ui.matplotlib(fig)
    return "Click two points on the image."

# 4️⃣ Cell that shows the JSON export button.
@mo.cell
def export_json():
    if not hasattr(annotate, "box"):
        return "No box defined yet."
    json_str = json.dumps(annotate.box, indent=2)
    return mo.ui.textarea(value=json_str, label="Bounding Box JSON", readonly=True)

# 5️⃣ Layout.
mo.ui.run(uploader, annotate, export_json)

This notebook demonstrates how Marimo can host interactive Matplotlib figures, capture user events, and propagate results to downstream cells. The final JSON can be copied into a training script, making the notebook a lightweight data‑labeling front‑end.

Real‑World Use Cases

  • Data‑Science Teams: Build internal dashboards that refresh automatically as new data lands, reducing the need for separate BI tools.
  • Machine‑Learning Ops: Create annotation or model‑inspection tools that live inside the same notebook used for training, ensuring reproducibility.
  • Education & Training: Instructors can design interactive lessons where students experiment with parameters and instantly see results, all within a single notebook.
  • Rapid Prototyping: Product managers can mock up UI flows with real data, share a link, and gather feedback without a full front‑end stack.

Data‑Science Teams

Imagine a nightly ETL job that writes a CSV to a shared folder. A Marimo dashboard can read that file, compute summary statistics, and display a Plotly heatmap—all refreshed automatically when the file changes. Because the dashboard lives in a .py notebook, version control is straightforward, and the same file can be run locally for debugging or deployed to a cloud server for wider access.

Education & Training

In a statistics class, an instructor can embed a Marimo notebook that lets students adjust the confidence level of a hypothesis test via a slider. The p‑value and confidence interval update in real time, turning abstract formulas into tangible visual feedback. Since the notebook can be exported as a static HTML page, students can explore the material offline.

Pro Tips & Best Practices

Tip 1 – Keep cells small and focused. Each cell should have a single responsibility (e.g., data loading, transformation, visualization). Smaller cells produce a clearer dependency graph and faster re‑execution when inputs change.
Tip 2 – Use explicit dependencies for clarity. While Marimo infers variable usage, you can pass widgets as function arguments to make the data flow obvious. This also helps static analysis tools and future collaborators understand the notebook’s logic.
Tip 3 – Cache heavy operations. For expensive API calls or large file reads, wrap the logic with @mo.cache (or functools.lru_cache) to avoid redundant execution when unrelated widgets update.
Tip 4 – Deploy with a single command. Once your notebook works locally, run marimo run my_notebook.py --host 0.0.0.0 --port 8080 to expose it behind a reverse proxy or container. No extra Dockerfile is needed unless you have system‑level dependencies.

Conclusion

Marimo bridges the gap between exploratory notebooks and production‑grade apps by introducing a reactive model that feels native to Python. Its minimal API, automatic dependency tracking, and seamless UI primitives let you build dashboards, annotation tools, and teaching modules with just a few lines of code. By keeping cells focused, caching expensive work, and leveraging built‑in widgets, you can scale prototypes into shareable web apps without ever leaving the notebook environment. Give Marimo a spin—your next data product might just start as a single, reactive Python notebook.

Share this article