Write and edit marimo reactive Python notebooks in the correct file format with proper cell structure, setup cells, reactivity patterns, and Typer CLI argument support. Use when the user asks to "create a marimo notebook", "edit a marimo file", "add marimo cells", "make a marimo script", "add CLI arguments to a marimo notebook", or when editing a Python file that imports marimo. Do NOT use for general Python scripting without marimo, for Jupyter notebooks, or for explaining marimo concepts without making code changes.
# Run as script (non-interactive, for testing)
uv run <notebook.py>
# Run interactively in browser
uv run marimo run <notebook.py>
# Edit interactively
uv run marimo edit <notebook.py>
Every marimo notebook MUST have a with app.setup: block immediately after app = marimo.App(...). Always import marimo as mo and typer in the setup cell:
app = marimo.App(width="medium")
with app.setup:
import marimo as mo
import typer
Add any other shared imports (numpy, polars, torch, etc.) to the setup cell as well.
Use in a dedicated cell:
mo.app_meta().mode == "script"@app.cell
def _(mo):
is_script_mode = mo.app_meta().mode == "script"
return (is_script_mode,)
Always create and show widgets. Only change the data source in script mode.
if not is_script_mode conditionals# Always show the widget
@app.cell
def _(mo):
scatter_widget = mo.ui.anywidget(ScatterWidget())
scatter_widget
return (scatter_widget,)
# Only change data source based on mode
@app.cell
def _(is_script_mode, scatter_widget):
if is_script_mode:
X, y = make_moons(n_samples=200, noise=0.2)
else:
X, y = scatter_widget.widget.data_as_X_y
return X, y
# Always show sliders - use .value in both modes
@app.cell
def _(mo):
lr_slider = mo.ui.slider(start=0.001, stop=0.1, value=0.01)
lr_slider
return (lr_slider,)
if StatementsMarimo's reactivity means cells only run when their dependencies are ready:
# BAD - the if statement prevents the chart from showing
@app.cell
def _(plt, training_results):
if training_results: # WRONG
fig, ax = plt.subplots()
ax.plot(training_results['losses'])
fig
return
# GOOD - let marimo handle the dependency
@app.cell
def _(plt, training_results):
fig, ax = plt.subplots()
ax.plot(training_results['losses'])
fig
return
# BAD - hiding errors behind try/except
@app.cell
def _(scatter_widget):
try:
X, y = scatter_widget.widget.data_as_X_y
except Exception as e:
return None, None
# GOOD - let it fail if something is wrong
@app.cell
def _(scatter_widget):
X, y = scatter_widget.widget.data_as_X_y
return X, y
Only use try/except for specific, expected exceptions with meaningful recovery.
Marimo only renders the final expression of a cell:
# BAD - indented expression won't render
@app.cell
def _(mo, condition):
if condition:
mo.md("This won't show!") # WRONG - indented
return
# GOOD - final expression renders
@app.cell
def _(mo, condition):
result = mo.md("Shown!") if condition else mo.md("Also shown!")
result
return
Use underscore prefix for loop variables to make them cell-private:
for _name, _model in items:
...
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "marimo",
# "typer",
# "torch>=2.0.0",
# ]
# ///
Always add typer when using CLI arguments.
# GOOD
from pathlib import Path
data_dir = Path(tempfile.mkdtemp())
parquet_file = data_dir / "data.parquet"
# BAD
import os
parquet_file = os.path.join(temp_dir, "data.parquet")
Run before delivering any notebook:
uvx marimo check <notebook.py>
uv --with marimo run python -c "import marimo as mo; help(mo.ui.form)"
Notebooks can accept --flags when run as uv run notebook.py using Typer. Use a module-level _cli_args dict to pass values from the CLI into cells. See references/TYPER.md for the full integration pattern and template.
Cells whose function names start with test_ are discovered as pytest tests. Cell arguments inject other cells' return values as test inputs:
pytest notebook.py # run all tests
pytest notebook.py -v # verbose
See references/PYTEST.md for fixtures, parametrize, and class-based test patterns.
Import polars as pl in the setup cell. Marimo renders DataFrames natively — drop a DataFrame as the final cell expression, or use:
mo.ui.table(df) — interactive table with row selectionmo.ui.dataframe(df) — no-code filter/groupby GUImo.ui.data_explorer(df) — column statistics and chart builderSee references/POLARS.md for the full API.
Import altair as alt in the setup cell. Wrap charts with mo.ui.altair_chart(chart) for reactivity — .value returns selected data as a DataFrame. See references/ALTAIR.md for chart types and composition.
Import numpy as np in the setup cell. Use for numerical computation, array math, and linear algebra. See references/NUMPY.md for the full API.
Import from scipy submodules in the setup cell (e.g., from scipy import stats, optimize). Use for statistics, optimization, signal processing, and integration. See references/SCIPY.md for the full API.
Import torch and torch.nn in the setup cell. Define models with nn.Module, train with autograd. See references/PYTORCH.md for the full API.
After the skill's primary task completes, run:
python3 ${PWD}/.claude/skills/skill-stat/scripts/record-stat.py "marimo-notebook"