Skip to content

PDF

Export marimo notebooks to PDF documents or slide decks.

PDF export works out-of-the-box on on molab.

Export to PDF from the marimo editor

Export to PDF from the notebooks action menu. This requires a few dependencies, which you will be prompted to install. It also requires LaTeX to install.

Download as PDF.

You can also export to PDF from the command palette (Ctrl/Cmd+K).

Export to PDF from the command line

You can export directly with marimo:

marimo export pdf notebook.py -o notebook.pdf

To exclude code cells:

marimo export pdf --no-include-inputs notebook.py -o notebook.pdf

To see all options, use

marimo export pdf --help

Rasterized output capture

Rasterized PNG fallback capture for marimo widget HTML (including anywidgets) and Vega outputs is enabled by default. Use --no-rasterize-outputs to disable it.

Use --raster-scale (range 1.0 to 4.0, default 4.0) to trade export speed/file size for sharper captured output. Use --raster-server=static (default) for a static capture page, or --raster-server=live to capture through a live notebook server.

Choose the raster server mode carefully

marimo gives you control over how output is captured. Use --raster-server=live when a widget needs Python to finish rendering. Otherwise, prefer the default --raster-server=static.

The notebook below is a concrete case where --raster-server=live helps.

Source code for examples/outputs/live_raster.py

Tip: paste this code into an empty cell, and the marimo editor will create cells for you

# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "anywidget==0.9.21",
#     "marimo>=0.20.2",
#     "traitlets==5.14.3",
# ]
# ///
import marimo

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


@app.cell(hide_code=True)
def _(CounterWidget):
    CounterWidget(count=42)
    return


@app.cell
def _(anywidget, os, traitlets):
    class CounterWidget(anywidget.AnyWidget):
        _esm = """
        export default async () => {
          let hostName = null;

          return {
            initialize({ model }) {
              // This message gets handled by _handle_custom_msg on the Python side
              model.send({ event: "requestHostName" });
            },

            render({ model, el }) {
              let count = () => model.get("count");
              let btn = document.createElement("button");
              btn.classList.add("counter-button");
              btn.innerHTML = `Initializing...`;

              // Set proper HTML content once message arrives from Python connection
              model.on("msg:custom", (msg, buffers) => {
                hostName = msg.response;
                btn.innerHTML = `count is ${count()} from ${hostName} host`;
              });

              btn.addEventListener("click", () => {
                model.set("count", count() + 1);
                model.save_changes();
              });

              model.on("change:count", () => {
                btn.innerHTML =
                  hostName
                    ? `count is ${count()} from ${hostName} host`
                    : `Initializing...`;
              });

              el.appendChild(btn);
            },
          };
        };
        """
        _css = """
        .counter-button {
          background: #387262;
          border: 0;
          border-radius: 10px;
          padding: 10px 50px;
          color: white;
        }
        """
        count = traitlets.Int(0).tag(sync=True)

        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.on_msg(self._handle_custom_msg)

        def _handle_custom_msg(self, *args, **kwargs):
            self.send({"response": os.name})

    return (CounterWidget,)


@app.cell(hide_code=True)
def _():
    import marimo as mo
    import anywidget
    import traitlets
    import os

    return anywidget, os, traitlets


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

This widget starts at Initializing... and then updates to count is ... from ... host after it receives data from Python. Static capture can freeze the initial placeholder; live capture gets the updated output.

# Static mode captures only the initial "Initializing..." placeholder
marimo export pdf examples/outputs/live_raster.py \
  -o live-raster-static.pdf --raster-server=static --no-sandbox --no-include-inputs

# Live mode captures the updated widget output
marimo export pdf examples/outputs/live_raster.py \
  -o live-raster-live.pdf --raster-server=live --no-sandbox --no-include-inputs

Rasterization dependencies

Rasterized output capture requires Playwright and Chromium:

uv pip install playwright
playwright install chromium

Slides

To export as a slides PDF, use the slides preset:

marimo export pdf notebook.py -o notebook.pdf --as=slides --raster-server=live

--raster-server=live is recommended for slide exports because it better preserves slide aspect ratio and captures widget-heavy outputs more reliably.

Available presets:

  • --as=document: Standard document PDF (default)
  • --as=slides: Slide-style PDF using reveal.js print layout

Export to PDF using Quarto

The marimo Quarto plugin enables exporting to PDF and other formats with Pandoc. See Publishing with Quarto for more details.

Export via Jupyter notebook

If you export to a Jupyter notebook, you can leverage various Jupyter ecosystem tools. For PDFs, you will need to have Pandoc and TeX installed. The examples below use uvx, which you can obtain by installing uv.

NOTEBOOK=notebook.ipynb

# Convert to PDF using nbconvert
uvx --with nbconvert --from jupyter-core jupyter nbconvert --to pdf $NOTEBOOK

# Convert to web PDF
uvx --with "nbconvert[webpdf]" --from jupyter-core jupyter nbconvert --to webpdf $NOTEBOOK --allow-chromium-download

# Convert to slides
uvx --with nbconvert --from jupyter-core jupyter nbconvert --to slides $NOTEBOOK

# Convert to rst with nbconvert
uvx --with nbconvert --from jupyter-core jupyter nbconvert --to rst $NOTEBOOK

# Generate PNG/PDF of specific cells using nbconvert
uvx --with nbconvert --with jupyter --from jupyter-core jupyter nbconvert --to pdf --execute --stdout $NOTEBOOK \
  --TemplateExporter.exclude_input=True

# Use nbconvert programmatically for more control
uv run --with nbconvert python -c "
from nbconvert import PDFExporter
import nbformat
nb = nbformat.read('$NOTEBOOK', as_version=4)
pdf_exporter = PDFExporter()
pdf_data, resources = pdf_exporter.from_notebook_node(nb)
with open('notebook.pdf', 'wb') as f:
    f.write(pdf_data)
"

You can also use other tools that work with Jupyter notebooks:

  • Quarto - Create beautiful documents, websites, presentations
  • nbgrader - Grade notebook assignments