Skip to content

Notebook dependencies

Marimo re-executes every notebook at build time to capture cell outputs. That means the Python environment running marimo-book build has to satisfy each notebook's import statements. marimo-book supports two strategies for provisioning those deps; pick in book.yml:

dependencies:
  mode: env       # default
  # mode: sandbox

env mode (default)

The Python environment that invoked marimo-book provides every notebook's deps. The typical setup:

# In your book's repo:
cat pyproject.toml
# [project]
# dependencies = ["marimo-book", "numpy", "pandas", "nltools", ...]

uv pip install -e .
marimo-book build

When this is the right choice:

  • All notebooks share the same dep stack (a tutorial book for one library, a course using one domain toolkit).
  • You're iterating fast in marimo-book serve — no overhead per rebuild.
  • You already manage a pyproject.toml / requirements.txt.

Tradeoffs:

  • If two notebooks need incompatible package versions, you're stuck.
  • Portability: the notebooks aren't self-contained; someone cloning the book needs to know what to install.

sandbox mode

Each notebook declares its own deps with a PEP 723 inline metadata header:

# /// script
# dependencies = [
#     "marimo>=0.23",
#     "numpy>=2.0",
#     "pandas>=2.0",
# ]
# ///

import marimo

__generated_with = "0.23.2"
app = marimo.App()
# ...

At build time, marimo-book passes --sandbox to marimo, which uses uv to provision a fresh isolated environment per notebook. After the first run, uv's package cache makes subsequent runs fast.

When this is the right choice:

  • Notebooks need incompatible dep versions.
  • You want portability — a reader can grab any single .py and run it anywhere uv is installed, no project file needed.
  • CI reproducibility — pinning exact versions in each notebook's header freezes behaviour.

Tradeoffs:

  • First build per notebook is ~5–10 s slower.
  • Requires uv to be installed on the build machine.

CLI override

You can force the mode for a single invocation with --sandbox or --no-sandbox:

marimo-book build --sandbox         # force sandbox even if book.yml says env
marimo-book build --no-sandbox      # force env even if book.yml says sandbox
marimo-book serve --sandbox         # slower loop, but reproducible

Common pattern: keep dependencies.mode: env in book.yml for local dev, then pass --sandbox in your CI workflow for the published build:

# .github/workflows/deploy.yml
- run: pip install marimo-book
- run: marimo-book build --sandbox --strict

Which mode is dartbrains using?

Dartbrains (the reference consumer) uses env mode. The repo has a pyproject.toml with the full scientific stack (nltools, numpy, nibabel, nilearn, …) and runs marimo-book from its own venv. That's the right call when all notebooks share the same dependencies, which is typical for a course or long-form tutorial.

Auto-generated PEP 723 blocks + WASM micropip bootstrap

marimo-book walks each notebook's AST, derives a dependency list (via marimo's own ~777-entry import → distribution mapping table), and uses it in two ways:

  1. A PEP 723 # /// script block is written at the top of a build-time staged copy of the notebook. This is the standard inline-script-metadata format read by uv run, marimo --sandbox, molab, and any other PEP-723-aware tool.
  2. For WASM-mode pages only, a micropip bootstrap cell is shipped to the browser inside marimo's islands JSON payload (<script type="application/vnd.marimo.islands+json">, marimo ≥ 0.24):
    try:
        import micropip
        await micropip.install(["nltools", "numpy", ...])
    except ImportError:
        pass
    marimo_book_micropip_done = True
    
    Every other cell's payload code is prefixed with _ = marimo_book_micropip_done, so marimo's dataflow runs the bootstrap strictly first. The cell has no visible output and no island of its own; the notebook the build executes is never modified for this. The install list is the import-derived dependencies merged with any hand-written dependencies in the notebook's own # /// script block, minus packages Pyodide already bundles (marimo's lockfile resolver supplies that list; set MARIMO_PYODIDE_LOCK_FILE to a local pyodide-lock.json for offline builds — if it can't be read, the full list is installed, which is safe but slower).

This is necessary because the marimo islands runtime only installs packages listed in a PEP 723 block of the notebook file it synthesizes in the browser from cell code — and that file never carries the block over. It still auto-loads Pyodide-bundled scientific packages (numpy/pandas/scipy/sklearn/matplotlib/nilearn/ nibabel/…) via loadPackagesFromImports, so only pure-Python PyPI-only deps (nltools is the canonical case) would fail without the bootstrap. Pages whose imports are all bundled (or marimo-only) get no payload at all.

Your source .py files are never modified by the build.

For WASM pages, both are unconditional — those pages don't work without them. For static or sandbox pages, opt in to the PEP 723 block via book.yml:

dependencies:
  mode: env
  auto_pep723: true                # generate blocks for static + sandbox pages too
  pin: env                         # optional: pin to currently-installed versions
  extras: ["nltools>=0.5"]         # always-include entries (NOT mapped through table)
  overrides:                       # manual import-name → distribution-name remappings
    my_internal_module: my-internal-pkg
  requires_python: ">=3.11"        # default: derive from running interpreter

Resolution rules

  • Stdlib filter uses sys.stdlib_module_names | sys.builtin_module_names, so os, pathlib, json, etc. are never listed.
  • marimo itself is filtered out (the islands runtime provides it).
  • Relative imports (from . import x) are skipped — they resolve locally, not on PyPI.
  • Distribution mapping uses marimo's own ~777-entry table — the same mapping the Pyodide kernel applies for fallback resolution. PILPillow, cv2opencv-python, sklearnscikit-learn, bs4beautifulsoup4, etc. Names absent from the table fall back to name.replace("_", "-"). User-supplied overrides win over both.
  • Extras are written verbatim alongside detected imports. An extra whose distribution name matches a detected import wins (so its version specifier is preserved).

marimo-book sync-deps

The build never modifies your .py files. To commit the auto-generated blocks back into your source (so they're under version control and work for molab, sharing, or anyone running the notebooks outside marimo-book), run:

marimo-book sync-deps                # writes/updates blocks in place
marimo-book sync-deps --check        # dry-run; exits non-zero if any file would change

The writer is non-destructive: existing requires-python, [tool.uv] tables, and hand-curated dependency entries are preserved. New detected dependencies are merged in by canonical distribution name. Running sync-deps twice in a row is a no-op.

sync-deps --check is suitable for a CI hook to keep notebook headers fresh against the rest of your project's deps.