From analytics
Creates, edits, runs, and converts marimo reactive Python notebooks with @app.cell decorators. Guides reactivity, CLI usage, UI components, SQL integration, and Jupyter migration for EDA.
npx claudepluginhub pymc-labs/python-analytics-skills --plugin analyticsThis skill uses the workspace's default tool permissions.
Marimo notebooks are reactive Python notebooks stored as pure `.py` files. Cells auto-execute when dependencies change, modeled as a directed acyclic graph (DAG).
Edits, debugs, verifies, and converts marimo reactive Python notebooks using strict cell rules, CLI commands, and output validation workflows.
Generates marimo notebooks as Python files with @app.cell structure for interactive apps, script mode testing, and browser editing.
Assembles marimo Python dashboard notebooks from analysis_plan.md specs with ibis queries and altair charts, validates with marimo check, manages dependencies, and launches.
Share bugs, ideas, or general feedback.
Marimo notebooks are reactive Python notebooks stored as pure .py files. Cells auto-execute when dependencies change, modeled as a directed acyclic graph (DAG).
list.append())—mutate in the same cell that creates the object, or create new variablesEach global variable must be defined by exactly one cell. Two strategies:
1. Wrap code in functions (preferred for reusable patterns):
@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:
@app.cell
def _(model1_data):
model1_transformed = model1_data.copy()
model1_transformed["new_col"] = model1_transformed["x"] * 2
return (model1_transformed,)
Never use underscore prefixes to generate unique variable names. No exceptions.
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:
@app.cellreturn (var1, var2,)def _(mo, df):def model_specification():# Create & Edit
marimo new # Create new notebook
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
# Convert
marimo convert notebook.ipynb -o notebook.py # Jupyter to marimo
# Export
marimo export html notebook.py -o out.html # Static HTML
marimo export ipynb notebook.py -o out.ipynb # To Jupyter
# Validate
marimo check notebook.py # Lint and validate
marimo check notebook.py --fix # Auto-fix issues
CRITICAL FOR TUTORIALS: By default, marimo run hides code. Use mo.show_code() to display it.
IMPORTANT: Call mo.show_code() as a statement on its own line, NOT in the return statement.
@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)
mu = alpha + beta * X
pm.Normal("y", mu=mu, sigma=1, observed=y)
# Show this cell's code alongside its output
mo.show_code(model, position="above")
return (model,)
WRONG vs RIGHT patterns:
# WRONG - do not put mo.show_code() in return statement
return mo.show_code(result)
# RIGHT - call as statement, then return separately
mo.show_code(result, position="above")
return (result,)
position="above" shows code first, then output (best for tutorials)position="below" shows output first, then code (default)@app.cell
def _(mo):
mo.md(r"""
# Title
Interpolate Python: {slider}
**LaTeX**: $f(x) = e^x$
$$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$
**Icons**: ::lucide:rocket:: or ::mdi:home::
""")
return
r"""...""") for LaTeXf"Value: {slider}"f"Plot: {mo.as_html(fig)}"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")
checkbox = mo.ui.checkbox(value=False, label="Enable")
dropdown = mo.ui.dropdown(["a", "b", "c"], value="a")
radio = mo.ui.radio(["option1", "option2"], value="option1")
multiselect = mo.ui.multiselect(["a", "b", "c"])
button = mo.ui.button(label="Click")
run_button = mo.ui.run_button(label="Run") # For triggering computation
table = mo.ui.table(df) # Interactive table with selection
dataframe = mo.ui.dataframe(df) # Editable dataframe
data_explorer = mo.ui.data_explorer(df) # No-code exploration
# Forms (require submit button)
form = mo.ui.text().form()
# Batch in markdown
form = mo.md("""
**Name**: {name}
**Age**: {age}
""").batch(
name=mo.ui.text(),
age=mo.ui.number(0, 120)
).form()
See references/ui_components.md for complete reference.
# Stacking
mo.hstack([el1, el2, el3], justify="center", gap=2)
mo.vstack([el1, el2, el3], align="start", gap=1)
# Containers
mo.accordion({"Section 1": content1, "Section 2": content2})
mo.tabs({"Tab 1": content1, "Tab 2": content2})
mo.callout(content, kind="info") # info, warn, success, danger, neutral
mo.sidebar([nav_content])
# Display
mo.tree({"a": {"b": 1}}) # Tree view
mo.stat(value="42", label="Users", caption="+5%")
mo.lazy(expensive_component) # Defer until visible
mo.output.replace(new_content) # Replace cell output
mo.output.append(additional) # Append to output
mo.output.clear() # Clear output
with mo.redirect_stdout():
print("This goes to cell output")
# Progress bar
for item in mo.status.progress_bar(items, title="Processing"):
process(item)
# Spinner
with mo.status.spinner(title="Loading..."):
load_data()
# 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()
# In-memory cache (session only)
@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)
See references/caching.md for model output caching patterns.
Warning: Use sparingly—over 99% of cases don't need mo.state().
@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 only when maintaining history, synchronizing UI bidirectionally, or introducing cycles.
# 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
fig = px.scatter(df, x="x", y="y")
interactive = mo.ui.plotly(fig)
interactive.value # Selected points
# Matplotlib interactive
fig, ax = plt.subplots()
ax.plot(x, y)
mo.mpl.interactive(fig)
Supported: Matplotlib, Seaborn, Plotly, Altair, Bokeh, HoloViews, hvPlot
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()
tour = mo.ui.anywidget(CellTour(
steps=[
{"cell_name": "intro", "title": "Welcome", "description": "..."},
{"cell_name": "model", "title": "Model", "description": "..."},
],
auto_start=False
))
See references/wigglystuff.md for all widgets.
model1_sigma, model2_sigmamo.lazy() for expensive components in tabs/accordions@mo.cache for session, @mo.persistent_cache for diskBEFORE making ANY edit to a marimo notebook:
marimo check notebook.py — Verify valid before and after editsAFTER completing edits:
print( — Replace ALL print statements with marimo output (mo.md(), mo.stat(), mo.callout())marimo check notebook.py — Verify no errors introducedNEVER use print() in cells: Print statements do NOT display in run mode. Always use:
mo.md(f"**Label:** {value}") — formatted textmo.stat(value=f"{x}", label="Label") — metric cardsmo.callout(content, kind="info") — callout boxesmo.show_code() must be called as a statement, not in return
Never delete output without replacing: When removing print statements, replace with equivalent marimo output
lambda v, i=i: ... not lambda v: ... imo.ui.array(), mo.ui.dictionary(), or mo.ui.batch()x: "SomeType"plt.tight_layout() before outputtingdotenv.load_dotenv(dotenv.find_dotenv(usecwd=True))