Marimo

Dash App Icon
Dash for macOS
Instant access to all the cheat sheets, API docs and snippets you need!

Marimo - A reactive Python notebook that is reproducible, git-friendly, and deployable as scripts or apps

Installation

Install (minimal)

pip install marimo
# or
uv add marimo
# or
conda install -c conda-forge marimo

Install (recommended extras)

pip install "marimo[recommended]"
# or
uv add "marimo[recommended]"

Includes DuckDB, Altair, Polars, Ruff, and more.

Verify installation with tutorial

marimo tutorial intro
# or with uv
uv run marimo tutorial intro

CLI Commands

Create and edit a notebook

marimo edit notebook.py

Creates a new notebook or opens an existing one in the browser.

Run notebook as app

marimo run notebook.py

Runs the notebook as a read-only web app.

Run notebook as a script

python notebook.py

Execute notebook cells top-to-bottom as a standard Python script.

Export to HTML

marimo export html notebook.py -o output.html

Export to script

marimo export script notebook.py -o script.py

Convert Jupyter notebook to Marimo

marimo convert notebook.ipynb -o notebook.py

New notebook (no file)

marimo edit

Opens a new untitled notebook in the browser.

Core Concepts

Import marimo

import marimo as mo

The single entry point for all Marimo features.

Reactive cells

Cells are connected via a directed acyclic graph based on variable definitions and references. When a cell runs, all downstream cells that depend on its variables automatically re-run.

  • No hidden state: deleting a cell removes its variables
  • Execution order is determined by data dependencies, not cell position

Displaying output

# The last expression in a cell is displayed automatically
mo.md("Hello, **world**!")

# Use mo.output.append() to display multiple outputs
mo.output.append(mo.md("First"))
mo.output.append(mo.md("Second"))

Mutation caveat

Marimo does not track object mutations. Mutating a variable in a different cell than where it was defined will not trigger reactive updates.

# BAD: define in cell A, mutate in cell B
# GOOD: define and mutate in the same cell
df["new_col"] = df["a"] + df["b"]  # same cell as df definition

Markdown & Display

Render markdown

mo.md("# Heading\n\nSome **bold** and _italic_ text.")

Interpolate Python values in markdown

name = "Alice"
mo.md(f"Hello, **{name}**!")

Embed UI elements or computed values directly into markdown.

Embed UI element in markdown

slider = mo.ui.slider(1, 10)
mo.md(f"Pick a value: {slider}")

Render LaTeX

# Inline math (use raw string for backslashes)
mo.md(r"The formula is $E = mc^2$")

# Display math
mo.md(r"$$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$")

Render icon

mo.icon("lucide:rocket")
mo.icon("lucide:leaf", size=20, color="green")

Uses the Iconify library. Format: "icon-set:icon-name".

Callout box

mo.callout(mo.md("**Warning:** This is important!"), kind="warn")
# kind: "info" | "success" | "warn" | "danger"

Stat display

mo.stat(value="42", label="Total users", caption="as of today")

Layout

Horizontal stack

mo.hstack([mo.md("Left"), mo.md("Right")])
mo.hstack([item1, item2], gap=2, justify="space-between")

Vertical stack

mo.vstack([mo.md("Top"), mo.md("Bottom")])
mo.vstack([item1, item2], gap=1, align="stretch")

Accordion (collapsible)

mo.accordion({
    "Section 1": mo.md("Content of section 1"),
    "Section 2": mo.md("Content of section 2"),
})

Tabs

tabs = mo.ui.tabs({
    "Tab A": mo.md("Content A"),
    "Tab B": mo.md("Content B"),
})
tabs
# Access selected tab: tabs.value

Sidebar

mo.sidebar(mo.md("Sidebar content"))

Align content

mo.center(mo.md("Centered text"))
mo.left(mo.md("Left-aligned"))
mo.right(mo.md("Right-aligned"))

Carousel

mo.carousel([mo.md("Slide 1"), mo.md("Slide 2"), mo.md("Slide 3")])

Lazy load

mo.lazy(expensive_computation())

Defers rendering until the element is visible in the viewport.

UI Inputs - Basic

Slider

slider = mo.ui.slider(start=0, stop=100, step=1, value=50)
slider
# Access value: slider.value

Range slider

range_slider = mo.ui.range_slider(start=0, stop=100, value=[20, 80])
range_slider
# Access value: range_slider.value  # [low, high]

Number input

num = mo.ui.number(start=0, stop=100, step=0.5, value=10)
num
# Access value: num.value

Text input

text = mo.ui.text(placeholder="Enter text...", label="Name")
text
# Access value: text.value

Text area

area = mo.ui.text_area(placeholder="Enter multiline text...", rows=5)
area
# Access value: area.value

Checkbox

cb = mo.ui.checkbox(label="Enable feature", value=False)
cb
# Access value: cb.value  # True or False

Switch

sw = mo.ui.switch(label="Dark mode", value=False)
sw
# Access value: sw.value  # True or False

Dropdown

dd = mo.ui.dropdown(
    options=["Option A", "Option B", "Option C"],
    value="Option A",
    label="Choose one",
)
dd
# Access value: dd.value

Radio buttons

radio = mo.ui.radio(
    options=["Red", "Green", "Blue"],
    value="Red",
    label="Pick a color",
)
radio
# Access value: radio.value

Multiselect

ms = mo.ui.multiselect(
    options=["A", "B", "C", "D"],
    label="Select multiple",
)
ms
# Access value: ms.value  # list of selected items

UI Inputs - Advanced

Button

btn = mo.ui.button(label="Click me", kind="success")
btn
# Access click count: btn.value

Run button (prevents auto-execution)

run_btn = mo.ui.run_button(label="Run computation")
run_btn
# Cell with run_btn.value only executes when button is clicked

Date picker

date = mo.ui.date(label="Pick a date")
date
# Access value: date.value  # datetime.date object

Date range picker

dr = mo.ui.date_range(label="Date range")
dr
# Access value: dr.value  # (start_date, end_date)

File upload

upload = mo.ui.file(label="Upload a file", filetypes=[".csv", ".json"])
upload
# Access contents: upload.contents()  # bytes
# Access name: upload.name()

File browser

browser = mo.ui.file_browser(initial_path=".", multiple=False)
browser
# Access path: browser.path()

Interactive table

table = mo.ui.table(
    data=[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}],
    selection="single",  # "single" | "multi" | None
)
table
# Access selected rows: table.value

Interactive dataframe

df_ui = mo.ui.dataframe(df)
df_ui
# Access transformed dataframe: df_ui.value

Code editor

editor = mo.ui.code_editor(value="print('hello')", language="python")
editor
# Access value: editor.value

Chat interface

def respond(messages, config):
    return "Echo: " + messages[-1].content

chat = mo.ui.chat(respond, prompts=["Hello", "Tell me a joke"])
chat

Form (submit on button click)

form = mo.ui.text(label="Your name").form()
form
# Access submitted value: form.value

Array of inputs

arr = mo.ui.array([mo.ui.slider(1, 10), mo.ui.slider(1, 10)])
arr
# Access values: arr.value  # list of values

Dictionary of inputs

d = mo.ui.dictionary({
    "x": mo.ui.slider(1, 10),
    "y": mo.ui.slider(1, 10),
})
d
# Access values: d.value  # {"x": ..., "y": ...}

Refresh button

refresh = mo.ui.refresh(default_interval="1s")
refresh
# Cell re-runs at the given interval while refresh is active

Interactive Plotting

Altair chart (interactive)

import altair as alt

chart = alt.Chart(df).mark_point().encode(x="x", y="y")
chart_ui = mo.ui.altair_chart(chart)
chart_ui
# Access selected points: chart_ui.value  # filtered DataFrame

Plotly chart (interactive)

import plotly.express as px

fig = px.scatter(df, x="x", y="y")
chart_ui = mo.ui.plotly(fig)
chart_ui
# Access selected data: chart_ui.value

Matplotlib figure

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3], [4, 5, 6])
mo.ui.matplotlib(fig)

State Management

Create reactive state

get_count, set_count = mo.state(0)

# Read value
current = get_count()

# Update value (triggers dependent cells to re-run)
set_count(current + 1)

State with button

get_count, set_count = mo.state(0)

increment = mo.ui.button(
    label="Increment",
    on_click=lambda _: set_count(get_count() + 1),
)
increment

Then in another cell: python mo.md(f"Count: **{get_count()}**")

Control Flow

Stop execution conditionally

mo.stop(condition, output=mo.md("Stopped because condition was met"))

Halts the current cell and downstream cells when condition is True.

Show loading spinner

with mo.status.spinner(title="Loading..."):
    result = long_running_function()

Progress bar

with mo.status.progress_bar(range(100)) as bar:
    for i in bar:
        do_work(i)

SQL

Query a DataFrame with SQL

In a SQL cell (toggle cell language to SQL), write: sql SELECT * FROM df WHERE age > 30

The result is automatically stored in the _df variable (or a custom variable name set in the cell options).

DuckDB query in Python

import duckdb

result = duckdb.sql("SELECT * FROM df WHERE age > 30").df()

Keyboard Shortcuts

Ctrl/Cmd-Enter

Run cell

Shift-Enter

Run cell and create new cell below

Ctrl/Cmd-Shift-Enter

Run all stale cells

Ctrl/Cmd-S

Save notebook

Ctrl/Cmd-Shift-P

Open command palette

Ctrl/Cmd-Shift-F

Find and replace

Ctrl/Cmd-Shift-K

Delete cell

Ctrl/Cmd-Shift-.

Interrupt kernel

Ctrl/Cmd-Shift-`

Toggle terminal

Ctrl/Cmd-Shift-M

Toggle minimap

Ctrl/Cmd-Shift-B

Toggle sidebar

Ctrl/Cmd-Shift-L

Format all cells (with Ruff)

Running as App

Run as web app

marimo run notebook.py

Serves the notebook as a read-only interactive app. Users can interact with UI elements but cannot edit code.

Run app on specific port

marimo run notebook.py --port 8080

Pass CLI arguments to notebook

# In notebook, access args with:
args = mo.cli_args()
name = args.get("name", "world")
mo.md(f"Hello, {name}!")
marimo run notebook.py -- --name Alice

Notes