Marimo notebook tests
Skill 0x0f0f0f/research-engineering-skills/skills/marimo-notebook-tests
Set up a Python project so files are simultaneously marimo interactive notebooks and pytest test modules — assertions are the source of truth, cells render the results. Use when the user wants notebooks that double as tests, asks to wire up marimo with pytest, or wants executable documentation/tutorials that stay green in CI.From its SKILL.md
npx -y skills add 0x0f0f0f/research-engineering-skills --skill marimo-notebook-testsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.1 KB, as published. Nobody here has run it
marimo-notebook-tests
Set up a Python project so that notebooks/*.py are simultaneously
marimo interactive notebooks and pytest test modules.
Assumes marimo is installed as a dev dependency alongside pytest.
Follow marimo's notebook authoring guidelines
This skill owns the notebook-as-test wiring — the test_* / @app.cell
split below. It does not redefine how marimo cells are written. That is
owned by marimo's official marimo-notebook skill. Load that skill by name and
always follow it when writing the cell bodies — it installs alongside this one
(bundled in the research-engineering-skills plugin; fetched as a sibling skill
by npx skills). Canonical upstream:
marimo-team/skills. Do not run npx skills add marimo-team/skills to reinstall it — it already ships alongside
this skill.
Apply its rules verbatim. The load-bearing ones, repeated here so they are never skipped:
- PEP 723 script metadata at the top of every notebook file
(
# /// script…# dependencies = ["marimo", …]…# ///). - Variables between cells define the reactivity — let marimo order cells
from their parameters; don't wrap cell bodies in needless
if/try. - Never mutate an object across cells — create a new object instead, so the dependency graph stays correct.
- Always create UI elements; only vary the data source by mode via
mo.app_meta().mode == "script"(synthetic/default data in script mode, widget values interactively). Don't conditionally hide widgets. - Render equations with
mo.md(r"""$...$""")so the notebook mirrors the paper's notation. - Don't over-prefix variables with underscores — reserve
_for a cell-local throwaway or two, not as a habit. - Run
marimo check <notebook.py>before committing to catch the common mistakes automatically.
When marimo's guidance and the wiring below ever disagree, marimo wins on cell
style; this skill only dictates the test_* / cell split and the execution
modes.
The pattern
Every notebook file has three layers:
import marimo
app = marimo.App(width="medium")
# 1. test_* functions at module level — importable, pytest-visible.
# Convention: return results so cells can reuse them for display
# without running the logic twice.
def test_something():
from mylib import thing
result = thing()
assert result == expected
return result
# 2. @app.cell functions — marimo reactive UI.
# mo is imported once in the first cell and flows as a dependency.
@app.cell
def _():
import marimo as mo
return (mo,)
@app.cell
def _(mo):
result = test_something() # assertion always runs inside the cell
return (mo.md(f"✓ result: {result}"),)
# 3. entry point — launches the notebook UI when run directly
if __name__ == "__main__":
app.run()
Execution modes
| command | behaviour |
|---|---|
pytest notebooks/ | finds test_* functions at module level, runs assertions |
marimo run notebooks/foo.py | interactive notebook; cells call test_* then render prose |
python notebooks/foo.py | launches the notebook UI |
from notebooks.foo import test_x | plain import, marimo never invoked |
pyproject.toml
[project.optional-dependencies]
test = ["pytest>=7", "marimo>=0.9"] # marimo is a dev dep; add your package(s) under test
[tool.pytest.ini_options]
testpaths = ["tests"] # default: fast unit tests only
# to include notebook integration tests: pytest tests/ notebooks/
Key invariants
test_*functions never import marimo — pure logic, no UI coupling.import marimo as molives in the first cell only; all subsequent cells receivemoas a parameter via marimo's dependency injection.@app.cellreturns aCellobject, not the original function, so pytest will not accidentally treat cell functions as tests.- Multiple cells named
_are fine: marimo registers each before the name is rebound; pytest ignores underscore-prefixed names. - Cells call
test_*first, then display — the assertion is the single source of truth, the cell just renders the result.
Where to put them
tests/tutorials/— preferred when the notebooks are tutorial-style tests for a library. Sits inside the existing test tree alongside unit tests; since pytest recurses intotests/, no extratestpathsconfig is needed.notebooks/— alternative if docs-primary identity is preferred.