Agent skill

marimo-notebook-dev

Create, edit, run, and convert marimo reactive Python notebooks. Use when working with .py marimo notebook files, creating reactive/interactive notebooks, exploratory data analysis in notebook format, or converting from Jupyter (.ipynb) to marimo. Marimo notebooks are pure Python files with @app.cell decorators that enable reactivity. Also covers wigglystuff widgets for advanced interactivity.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/marimo-notebook-dev-fonnesbeck-marimo-notebook-dev

SKILL.md

Marimo Notebooks

Marimo notebooks are reactive Python notebooks stored as pure .py files. Cells auto-execute when dependencies change, modeled as a directed acyclic graph (DAG).

Core Concepts

Reactivity Model

  • marimo uses static analysis to build a dependency graph from variable references and definitions
  • When a cell runs, all cells referencing its defined variables automatically re-run
  • Execution order follows the dependency graph, not visual cell order
  • Each global variable must be defined by exactly one cell
  • marimo does not track object mutations (like list.append())—mutate in the same cell that creates the object, or create new variables

Avoiding Variable Name Conflicts

Each global variable must be defined by exactly one cell. Two strategies to avoid conflicts:

1. Wrap code in functions (preferred for reusable patterns):

python
@app.cell
def _(data):
    def compute_mean_with_new_col(df):
        temp = df.copy()
        temp["new_col"] = temp["x"] * 2
        return temp.mean()

    return (compute_mean_with_new_col(data),)

2. Use meaningful, unique variable names:

python
@app.cell
def _(model1_data):
    model1_transformed = model1_data.copy()
    model1_transformed["new_col"] = model1_transformed["x"] * 2
    return (model1_transformed,)

@app.cell
def _(model2_data):
    model2_transformed = model2_data.copy()
    model2_transformed["new_col"] = model2_transformed["x"] * 2
    return (model2_transformed,)

Notebook Structure

python
import marimo

__generated_with = "0.10.0"
app = marimo.App(width="medium")

@app.cell
def _():
    import marimo as mo
    return (mo,)

@app.cell
def _(mo):
    mo.md("# Hello")
    return

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

Key rules:

  • Each cell is a function decorated with @app.cell
  • Variables shared by returning tuples: return (var1, var2,)
  • Cells receive variables as parameters: def _(mo, df):
  • Execution order follows dependency graph, not position
  • Name cells descriptively for CellTour targeting: def model_specification():

CLI Commands

bash
# Create & Edit
marimo new                           # Create new notebook
marimo new "build a dashboard"       # AI-generated starter
marimo edit notebook.py              # Open editor
marimo edit notebook.py --watch      # Live reload on file changes

# Run as App
marimo run notebook.py               # Run as app (code hidden by default)
marimo run notebook.py --include-code  # Show code in app view
marimo run notebook.py --host 0.0.0.0 --port 8080  # Network accessible
marimo run notebook.py --base-url /subpath  # Deploy at subpath
marimo run notebook.py --headless    # No browser auto-open

# Run as Script
python notebook.py                   # Execute as Python script
python notebook.py -- --arg1 val1    # Pass CLI arguments (after --)

# Convert
marimo convert notebook.ipynb -o notebook.py  # Jupyter to marimo
marimo convert notebook.md -o notebook.py     # Markdown to marimo

# Export
marimo export html notebook.py -o out.html              # Static HTML
marimo export html notebook.py -o out.html --include-outputs  # With outputs
marimo export html-wasm notebook.py -o out.html         # Interactive WASM
marimo export ipynb notebook.py -o out.ipynb            # To Jupyter
marimo export ipynb notebook.py -o out.ipynb --include-outputs
marimo export script notebook.py -o out.py              # Pure Python
marimo export md notebook.py -o out.md                  # Markdown

# Utilities
marimo check notebook.py             # Lint and validate
marimo check notebook.py --fix       # Auto-fix issues
marimo tutorial intro                # Interactive tutorials
marimo tutorial markdown             # Markdown tutorial
marimo tutorial plots                # Plotting tutorial
marimo tutorial layout               # Layout tutorial

Code Visibility in Run Mode

CRITICAL FOR TUTORIALS: By default, marimo run hides code. Use these options to show code:

mo.show_code() - Per-Cell Display (Recommended)

python
@app.cell
def model_definition(mo, pm, X, y):
    with pm.Model() as model:
        alpha = pm.Normal("alpha", mu=0, sigma=10)
        beta = pm.Normal("beta", mu=0, sigma=10)
        sigma = pm.HalfNormal("sigma", sigma=1)
        mu = alpha + beta * X
        likelihood = pm.Normal("y", mu=mu, sigma=sigma, observed=y)

    # Show this cell's code alongside its output
    return mo.show_code(model)

@app.cell
def sampling(mo, pm, model):
    with model:
        trace = pm.sample(1000)
    # Position: "above" or "below" (default)
    return mo.show_code(trace, position="above")

Key points:

  • mo.show_code(output) - shows code below output (default)
  • mo.show_code(output, position="above") - shows code above output
  • In displayed code, mo.show_code(...) is replaced with ...
  • Use for EVERY code cell in tutorials/educational notebooks

CLI Flag - Show All Code

bash
marimo run notebook.py --include-code

URL Parameter - For Hosted Apps

https://your-app.marimo.app/app?show-code=true

Markdown with mo.md()

python
@app.cell
def _(mo):
    mo.md(r"""
    # Title

    Interpolate Python: {slider}

    **LaTeX inline**: $f(x) = e^x$

    **LaTeX block**:
    $$
    \int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
    $$

    **Icons** (Iconify): ::lucide:rocket:: or ::mdi:home::
    """)
    return

Key features:

  • Use raw strings (r"""...""") for LaTeX to avoid escaping backslashes
  • Interpolate UI elements directly: f"Value: {slider}"
  • For complex objects, use mo.as_html(): f"Plot: {mo.as_html(fig)}"
  • Load LaTeX macros: mo.latex(filename="macros.tex")
  • Icons: mo.icon("lucide:rocket", size=20, color="blue")

UI Components (mo.ui.*)

Basic Inputs

python
slider = mo.ui.slider(0, 100, value=50, label="Value")
number = mo.ui.number(0, 100, value=50)
text = mo.ui.text(value="", placeholder="Enter text")
text_area = mo.ui.text_area(value="", placeholder="Multi-line")
checkbox = mo.ui.checkbox(value=False, label="Enable")
switch = mo.ui.switch(value=False, label="Toggle")
dropdown = mo.ui.dropdown(["a", "b", "c"], value="a")
radio = mo.ui.radio(["option1", "option2"], value="option1")
multiselect = mo.ui.multiselect(["a", "b", "c"])
range_slider = mo.ui.range_slider(0, 100, value=(20, 80))

Date/Time

python
date = mo.ui.date(value="2024-01-01")
datetime = mo.ui.datetime()
date_range = mo.ui.date_range()

Buttons

python
button = mo.ui.button(label="Click")              # Simple button
run_button = mo.ui.run_button(label="Run")        # For triggering computation
refresh = mo.ui.refresh(default_interval="1s")    # Auto-refresh timer

# Counter button
counter = mo.ui.button(value=0, on_click=lambda count: count + 1)

# Toggle button
toggle = mo.ui.button(value=False, on_click=lambda v: not v)

Files & Media

python
file = mo.ui.file(kind="file", filetypes=[".csv", ".json"])
file_browser = mo.ui.file_browser(initial_path="./")
microphone = mo.ui.microphone()

Data Components

python
table = mo.ui.table(df)                    # Interactive table with selection
dataframe = mo.ui.dataframe(df)            # Editable dataframe
data_editor = mo.ui.data_editor(df)        # Full editor
data_explorer = mo.ui.data_explorer(df)    # No-code exploration

Advanced

python
code_editor = mo.ui.code_editor(value="", language="python")
chat = mo.ui.chat(model)                   # Chat interface
anywidget = mo.ui.anywidget(MyWidget())    # Custom widgets
tabs = mo.ui.tabs({"Tab1": content1, "Tab2": content2})

Grouping UI Elements

python
# Dynamic arrays
array = mo.ui.array([mo.ui.slider(0, 10) for _ in range(5)])
array.value  # List of values

# Dynamic dictionaries
dictionary = mo.ui.dictionary({"x": mo.ui.slider(0, 10), "y": mo.ui.slider(0, 10)})
dictionary.value  # Dict of values

# Forms (require submit button)
form = mo.ui.text().form()
form.value  # None until submitted

# Batch multiple elements in markdown
form = mo.md("""
**Name**: {name}
**Age**: {age}
""").batch(
    name=mo.ui.text(),
    age=mo.ui.number(0, 120)
).form()

Layout Functions

python
# Stacking
mo.hstack([el1, el2, el3], justify="center", gap=2)  # Horizontal
mo.vstack([el1, el2, el3], align="start", gap=1)     # Vertical

# Containers
mo.accordion({"Section 1": content1, "Section 2": content2})
mo.tabs({"Tab 1": content1, "Tab 2": content2})  # Use mo.ui.tabs for state
mo.callout(content, kind="info")  # info, warn, success, danger, neutral
mo.carousel([item1, item2, item3])
mo.sidebar([nav_content])

# Alignment
mo.center(content)
mo.left(content)
mo.right(content)

# Display
mo.plain(content)           # No styling
mo.tree({"a": {"b": 1}})    # Tree view
mo.json(data)               # JSON viewer
mo.stat(value="42", label="Users", caption="+5%")  # Statistic card

# Lazy loading (defer until visible)
mo.lazy(expensive_component)                      # Object
mo.lazy(lambda: compute_expensive())              # Function (deferred)
mo.lazy(async_func, show_loading_indicator=True)  # Async with spinner

# Outline/Table of Contents
mo.outline()  # Creates navigation outline

# Routing (multi-page apps)
mo.routes({
    "#/": render_home,
    "#/about": render_about,
    mo.routes.CATCH_ALL: render_home,
})

# Combined with navigation
mo.sidebar([
    mo.nav_menu({
        "#/": f"{mo.icon('lucide:home')} Home",
        "#/about": f"{mo.icon('lucide:user')} About",
    }, orientation="vertical")
])

Output Functions

python
# Dynamic output manipulation
mo.output.replace(new_content)      # Replace cell output
mo.output.append(additional)        # Append to output
mo.output.clear()                   # Clear output
mo.output.replace_at_index(val, 2)  # Replace at specific index

# Console redirection
with mo.redirect_stdout():
    print("This goes to cell output")

with mo.capture_stdout() as buffer:
    print("Captured")
    text = buffer.getvalue()

Status Indicators

python
# Progress bar (like tqdm)
for item in mo.status.progress_bar(items, title="Processing"):
    process(item)

# With manual control
with mo.status.progress_bar(total=100, title="Loading") as bar:
    for i in range(100):
        do_work()
        bar.update(title=f"Step {i}")

# Spinner
with mo.status.spinner(title="Loading...", subtitle="Please wait"):
    load_data()

# Conditional spinner
mo.status.spinner(title="Loading") if loading else mo.md("Done!")

Control Flow

python
# Stop execution conditionally
mo.stop(condition, mo.md("*Message when stopped*"))

# Example: require button click
run_button = mo.ui.run_button()
mo.stop(not run_button.value, mo.md("Click Run to execute"))
expensive_computation()

# Example: require form submission
mo.stop(form.value is None, mo.md("Submit the form first"))
process(form.value)

# Conditional output
result if condition else None

Caching

python
# In-memory cache (session only, fastest)
@mo.cache
def expensive_function(x, y):
    return compute(x, y)

# Persistent cache (survives restarts)
@mo.persistent_cache(name="embeddings")
def compute_embeddings(text):
    return model.encode(text)

# Block-style persistent cache
with mo.persistent_cache(name="data_load"):
    df = load_large_dataset()
    processed = transform(df)

# LRU cache with size limit
@mo.lru_cache(maxsize=128)
def bounded_cache(x):
    return compute(x)

State Management

Warning: Use sparingly—over 99% of cases don't need mo.state().

python
@app.cell
def _(mo):
    get_count, set_count = mo.state(0)
    return get_count, set_count

@app.cell
def _(mo, get_count, set_count):
    mo.ui.button(
        label=f"Count: {get_count()}",
        on_click=lambda _: set_count(lambda n: n + 1)
    )
    return

Use mo.state() only when:

  • Maintaining history tied to UI elements
  • Synchronizing multiple UI elements bidirectionally
  • Introducing cycles across cells at runtime

SQL Integration

Install: pip install "marimo[sql]"

python
@app.cell
def _(mo, threshold):
    # Query Python dataframes by name
    result = mo.sql(f"""
        SELECT * FROM my_dataframe
        WHERE value > {threshold.value}
        LIMIT 100
    """)
    return (result,)

# External sources
mo.sql("""
    SELECT * FROM 's3://bucket/file.parquet';
    SELECT * FROM read_csv('data.csv');
""")

SQL can query: Python dataframes (polars/pandas), DuckDB, PostgreSQL, MySQL, SQLite, Snowflake, BigQuery, Redshift, CSV/Parquet files, S3

Escape literal braces with {{}}:

sql
SELECT unnest([{{'a': 42, 'b': 84}}]);

Output types: native (DuckDB relation), lazy-polars, polars, pandas, auto

Interactive Plotting

python
# Altair with selection
chart = alt.Chart(df).mark_point().encode(x="x", y="y")
selection = mo.ui.altair_chart(chart, chart_selection="point")
selected_data = selection.value  # DataFrame of selected points

# Plotly (scatter, treemap, sunburst)
fig = px.scatter(df, x="x", y="y")
interactive = mo.ui.plotly(fig)
interactive.value  # Selected points

# Matplotlib interactive (pan, zoom, hover)
fig, ax = plt.subplots()
ax.plot(x, y)
mo.mpl.interactive(fig)

Supported libraries: Matplotlib, Seaborn, Plotly, Altair, Bokeh, HoloViews, hvPlot, Leafmap

Media Functions

python
mo.image(src)          # URL, path, or file-like object
mo.image_compare(img1, img2)  # Side-by-side comparison
mo.video(src)
mo.audio(src)
mo.pdf(src)
mo.download(data, filename="file.csv", label="Download")
mo.plain_text(text)

External Data Access

python
# CLI arguments (after --)
args = mo.cli_args()  # Dict of arguments
size = mo.cli_args().get("size") or 100

# URL query parameters
params = mo.query_params()
params["key"]  # Read
params.set("key", "value")  # Write (updates URL, triggers reactivity)

# File watching (reactive to changes)
watched_file = mo.watch.file("config.json")  # Re-runs on file change
watched_dir = mo.watch.directory("data/")     # Re-runs on structure change

App Metadata

python
meta = mo.app_meta()
meta.mode   # "edit", "run", "script", "test", or None
meta.theme  # "light" or "dark"
meta.request  # HTTP request info (headers, cookies, params)

Embedding Notebooks

python
# Import another notebook
from other_notebook import app as other_app

@app.cell
async def _(other_app):
    # Must call from different cell than import
    result = await other_app.embed()
    return result.output, result.defs

# Run programmatically with overrides
outputs, defs = other_app.run(defs={"param": value})

HTML Manipulation

python
html = mo.Html("<div>Custom HTML</div>")
html.text  # Get underlying string

# Styling
styled = html.style({"color": "red", "font-size": "20px"})
styled = html.callout(kind="info")

# iframes (for scripts that need execution)
mo.iframe(html_with_scripts, width="100%", height="400px")

Best Practices

  1. Wrap reusable code in functions - Keeps intermediate variables local and avoids namespace pollution
  2. Use meaningful, unique variable names - e.g., model1_sigma, model2_sigma instead of generic names
  3. Don't mutate across cells - Mutate in same cell or create new variables
  4. Write idempotent cells - Same inputs should produce same outputs
  5. Use mo.stop() - Gate expensive operations behind conditions/buttons
  6. Split into modules - Import from .py files, use auto-reload
  7. Use lazy loading - mo.lazy() for expensive components in tabs/accordions
  8. Cache expensive ops - @mo.cache for session, @mo.persistent_cache for disk

Common Gotchas

  • Closures in loops: Use default args lambda v, i=i: ... not lambda v: ... i
  • on_change handlers: Only work if element is bound to global variable
  • Dynamic UI elements: Must wrap in mo.ui.array(), mo.ui.dictionary(), or mo.ui.batch()
  • Type annotations: Registered as references unless quoted: x: "SomeType"
  • matplotlib cut-off: Call plt.tight_layout() before outputting
  • dotenv: Use dotenv.load_dotenv(dotenv.find_dotenv(usecwd=True))

Configuration

Environment Variables

  • MARIMO_OUTPUT_MAX_BYTES (default 8MB)
  • MARIMO_STD_STREAM_MAX_BYTES (default 1MB)
  • MARIMO_SKIP_UPDATE_CHECK
  • MARIMO_SQL_DEFAULT_LIMIT

User Config (.marimo.toml)

  • Runtime: autorun, lazy mode, SQL output format
  • Display: theme, font size, output placement
  • Editor: hotkeys, vim mode, copilot, formatting

App Config (in notebook file)

  • Width, title, custom CSS, HTML head

Deployment

bash
# Docker
marimo run notebook.py --host 0.0.0.0 --port 8080

# Health endpoints
/health    # 200 OK
/healthz   # Alternative
/api/status  # JSON status

Platforms: Docker, Kubernetes, HuggingFace, Railway, SkyPilot, Slurm/HPC

Testing

bash
# Run as pytest
pytest notebook.py

# With doctest
python -m doctest notebook.py

marimo notebooks are pure Python—test like any Python module.

Wigglystuff Widgets

Install: pip install wigglystuff

python
from wigglystuff import Slider2D, Paint, SortableList, Matrix, CellTour
import marimo as mo

slider2d = mo.ui.anywidget(Slider2D())
slider2d.x, slider2d.y

paint = mo.ui.anywidget(Paint(width=400, height=300))
paint.to_pil()

sortable = mo.ui.anywidget(SortableList(items=["A", "B", "C"]))
sortable.items

matrix = mo.ui.anywidget(Matrix(rows=3, cols=3))
matrix.data

# CellTour for guided tutorials
tour = mo.ui.anywidget(CellTour(
    steps=[
        {"cell_name": "intro", "title": "Welcome", "description": "...", "position": "bottom"},
        {"cell_name": "model", "title": "Model", "description": "...", "position": "bottom"},
    ],
    auto_start=False,
    show_progress=True
))

Available: Slider2D, Matrix, Paint, EdgeDraw, SortableList, ColorPicker, KeystrokeWidget, GamepadWidget, WebkitSpeechToTextWidget, CopyToClipboard, WebcamCapture, CellTour

See references/wigglystuff.md for complete widget reference.

Resources

Didn't find tool you were looking for?

Be as detailed as possible for better results