Skip to main content

sphinx-examples-as-code

A Sphinx extension that turns docstring/page "Examples" sections into downloadable, runnable Python scripts, Jupyter notebooks and/or marimo notebooks, with a download link inserted into the section.

Pages or docstrings without an Examples section are left completely untouched. Adding sphinx_examples_as_code to conf.py's extensions is the only on/off switch.

Installation

pip install sphinx-examples-as-code

Sphinx-Gallery integration is opt-in and needs a compatible sphinx-gallery, which the gallery extra pins:

pip install 'sphinx-examples-as-code[gallery]'

marimo notebook downloads need marimo 0.14 or newer, which the marimo extra installs:

pip install 'sphinx-examples-as-code[marimo]'

Add it to your Sphinx conf.py:

extensions = [
    ...,
    'sphinx_examples_as_code',
]

Configuration

Everything lives in one dict in conf.py, sphinx_examples_as_code_conf -- set only the keys you want to change from their default:

sphinx_examples_as_code_conf = {
    'link_position': 'top',
    'formats': ['python', 'jupyter', 'marimo'],
    'gallery_downloads': False,
    'footer': (
        'Generated by `sphinx-examples-as-code '
        '<https://github.com/pyvista/sphinx-examples-as-code>`_'
    ),
    'link_labels': {
        'python': 'Download Python script',
        'jupyter': 'Download Jupyter notebook',
        'marimo': 'Download marimo notebook',
    },
    'script_metadata': False,
    'dependencies': {},
    'requires_python': None,
    'export_dir': None,
    'export_sources': ['gallery'],
}
  • link_position: where the download link(s) land within the Examples section. 'top' (default) or 'bottom'.
  • formats: which downloads to generate. A list containing any of 'python' (a plain Python script, <name>.py), 'jupyter' (a Jupyter notebook, <name>.ipynb) and 'marimo' (a marimo notebook, <name>_marimo.py); defaults to all three. Always offered in that order regardless of how the list is written. Without marimo 0.14 or newer installed, the default skips the marimo download, and a formats that names 'marimo' (in conf.py or through -D) stops the build with a configuration error. An unknown format raises a configuration error too; the old 'py' and 'ipynb' names say what replaced them. <name> is the documented object's full name without its top-level package, e.g. Plotter_show for pyvista.Plotter.show, or the gallery example's file name.
  • link_labels: the text of the download link(s) themselves, per format. Set only the format(s) you want to change; any left unset keep reading their own default shown above.
  • gallery_downloads: opt-in takeover of sphinx-gallery's own per-example downloads. False (default) leaves sphinx-gallery pages untouched. See Sphinx-Gallery integration below.
  • footer: a string appended to the end of every generated file. Defaults to a one-line "generated file" credit linking back to this project; set to None to omit it entirely. Preceded by a blank line and a --only divider line, which also renders as a real horizontal rule in notebooks; the footer always gets its own dedicated cell there. Parsed as RST: a hyperlink written as `text <url>`_ becomes a real clickable Markdown link in notebooks, and renders inline as text url in Python scripts. Plain text with no markup at all becomes one comment line per line of text, and blank-line-separated paragraphs stay separated.
  • script_metadata: True adds each download's dependencies and a one-line run command. See Dependencies and run commands below. False (default) adds neither.
  • dependencies: extra PEP 508 requirements per format, added to the detected ones, e.g. {'jupyter': ['pyvista[jupyter]']}. An entry naming a detected project replaces it, so {'python': ['pyvista>=0.46']} sets its version. An entry without a version keeps the automatic pin described below. Only used with script_metadata on.
  • requires_python: a requires-python specifier for the dependency block, e.g. '>=3.10'. None (default) derives one from the dependencies instead; see Dependencies and run commands. Only used with script_metadata on.
  • export_dir: a folder that also receives a copy of each download from the sources in export_sources, at <export_dir>/<source>/<format>/<folder>/<file>, e.g. gallery/jupyter/auto_examples/plot_minimal.ipynb for the gallery page auto_examples/plot_minimal. <folder> is the folder of the page the download is on. A relative path is relative to the folder holding conf.py. None (default) exports nothing. Only HTML builders export, and only for the pages they write. The folder is never cleared, so build with -E into an empty folder for a complete, current copy. Two downloads that would land on the same path raise a warning, and the later one is kept.
  • export_sources: which downloads export_dir copies. A list containing any of 'gallery' (sphinx-gallery pages, only with gallery_downloads on) and 'docstrings' (Examples sections); defaults to ['gallery'].

An unrecognized key raises a configuration error at build start.

Cross-references and hyperlinks resolve into absolute links using Sphinx's own html_baseurl. Leave it unset and no links are generated anywhere.

Overriding a single key from the command line works via a dotted -D flag, e.g. -D sphinx_examples_as_code_conf.link_position=bottom -- this only touches that one key, leaving the rest (conf.py's values, or the defaults) alone.

Dependencies and run commands

With script_metadata on, each download lists the third-party projects its code imports, plus any dependencies set for its format, and says how to run it:

  • Python scripts start with a PEP 723 # /// script block, followed by # Run this script with: uv run <name>.py.
  • Jupyter notebooks say Open this notebook with: `uvx --python "<requires-python>" --from jupyterlab --with <dependency> ... jupyter-lab <name>.ipynb` in their first cell, with --python only when there is a requires-python. Arguments are double-quoted where needed, so the command works in bash, zsh, PowerShell and cmd. An argument containing ", $, a backtick, \, % or a ! not followed by = is single-quoted instead, which cmd does not understand.
  • marimo notebooks start with the same # /// script block, which also lists marimo, followed by # Open this notebook with: uvx marimo edit --sandbox <name>_marimo.py.
  • Each run command is followed by a line linking to uv's installation guide.

Imports are read from the code, not run. Each imported top-level module maps to the project that provides it in the environment building the docs; standard-library modules, relative imports and modules no single installed project provides are left out. Projects the code needs but never imports, such as an optional backend, go in dependencies, or are imported explicitly in the example. In Python scripts and Jupyter notebooks, a dependency with no version or extras of its own is left out when another listed one already installs it on every platform, e.g. numpy next to pyvista. marimo notebooks list every dependency, since marimo's sandbox adds back any it does not find by name.

The project named by Sphinx's own project setting (e.g. project = 'PyVista') gets extra treatment when an example imports it and it is installed: its dependency becomes <name>>=X.Y, the installed release's major and minor version, or plain <name> for a pre-release or dev build.

Unless requires_python is set, the block's requires-python combines:

  • that project's own Requires-Python, e.g. >=3.10, when it is listed;
  • <3.N, when the download depends on that project, or on any other dependency, listed or left out, installed from a wheel built for one Python version's ABI, e.g. cp314-cp314, in the environment building the docs. N is one minor version above the Python building the docs, or above the oldest of the newest Pythons their Programming Language :: Python :: 3.X classifiers name, whichever is newer; one naming no 3.X doesn't count toward that oldest. Dependencies whose wheels carry the none or stable abi3 ABI tag don't count, and the bound is dropped when it would exclude every Python the project's own Requires-Python allows.

With neither, the line is left out.

Conversion rules

What happens to the content of an Examples section:

  • Doctest blocks (>>> ... / ... ...) keep their input lines, prompts stripped, as real Python source. Doctest output lines are dropped. In Python scripts, an expression that has output is wrapped in print(repr()), so running the script prints what the doctest shows. A call that prints its own output and returns None also prints None. In notebooks, a statement that has output ends its code cell. An example flagged # doctest: +SKIP becomes comments, other # doctest: flags are removed, and each example may sit at its own indent.
  • .. code-block:: python (or py) blocks are kept as-is, unless they start with >>>, which converts them like a doctest block; other languages become comments, set off with blank lines on both sides like any other directive.
  • Admonitions (.. note::, .. warning::, ...) become a # LABEL: comment followed by their content as comments, indented one level under the label in Python scripts only.
  • A bullet/numbered list becomes a -/N.-marked line per item, set off with a blank line on both sides in either format — a bare # instead of a real blank line when that falls inside an admonition or a definition's own body in a Python script, so the whole thing still reads as one unbroken comment block. A definition list's term stays at the surrounding indent; its definition (the body nested under it) is indented one level further in Python scripts, same as an admonition's own content.
  • Cross-references and inline code (:class:, :meth:, :func:, :attr:, double-backtick literals, ...) keep their display text, wrapped in backticks (e.g. :class:`pyvista.Plotter` -> `pyvista.Plotter`). If html_baseurl is set and the reference resolves, notebooks turn it into a clickable link; Python scripts never write the link, only the display text.
  • Plain prose-style references (:ref:, :doc:) are treated the same way, minus the backticks.
  • Everything else text-bearing (prose, captions, other non-Python code) becomes a plain # comment.
  • A table becomes an aligned text table, its caption on the line above: an RST simple table in Python scripts, a Markdown pipe table in notebooks. Each cell is flattened to a single line, and a column left entirely empty -- one holding only images, say -- is dropped along with its heading.
  • Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' .. tags:: line are dropped entirely.
  • "See Also" content is always dropped.

Generated Python scripts start with a # Examples from <qualified name> title header (gallery mode uses the page's own title instead -- see below), with a few whitespace conventions: prose directly above a code block stays attached to it, a code block is always followed by a blank line, and a directive (header, # NOTE:-style block) gets blank lines on both sides.

Generated Jupyter notebooks use the same content, split into alternating code/markdown cells instead.

Generated marimo notebooks are that same Jupyter notebook converted by marimo's own converter, the one behind marimo convert. from __future__ imports are dropped first, so the notebook also runs as a plain script (python <name>_marimo.py).

A download link is only added if the resulting code contains at least one real executable statement.

With sphinx_examples_as_code_conf['gallery_downloads'] = True, this extension takes over the downloads on every page generated by sphinx-gallery: the "Go to the end to download the full example code" note and the .py/.zip download footer are removed from the page, replaced with download link(s) built by this extension instead — same conversion rules as above, applied to the whole page rather than one Examples section. The generated file's header uses the page's own title (e.g. # Create Circular Arcs) rather than the generic # Examples from <docname>.

The "Total running time" line and the "Gallery generated by Sphinx-Gallery" credit line stay on the rendered page untouched, but never make it into the generated download itself.

Detection is automatic and per-page — any page without a sphinx-gallery download footer is left completely untouched. Gallery pages and ordinary docstring/prose pages (using the Examples-section behavior above) can coexist on the same site.

A # %% cell with its own RST heading gets the same header treatment as the file's own title, and renders as a real Markdown heading in notebooks. Level is relative to actual RST section nesting, not always one below the file's own header: a cell heading nested under the page's own title (the common case) is one level below it; one that reuses the page title's own underline character is level 1, the same as the file's own header; a cell heading nested under that is level 2 relative to it, and so on.

Each format uses one heading style consistently across every level, rather than mixing styles:

  • Python scripts use an RST-style title + underline, one character per level -- the same sequence Sphinx's own documentation uses for sections through sub-paragraphs: =, -, ~, ^, ", ' for levels 1 through 6.
  • Notebooks always use ATX syntax (#, ##, ###, ...) at every level.

Two things worth knowing before turning this on:

  • It's built against sphinx-gallery's own doctree output — the sphx-glr-* CSS classes its own theming depends on, and the private node type holding a highlighted code block — not a documented extension API. A future sphinx-gallery release could shift that structure without warning. Only sphinx-gallery 0.22.x is tested, so the gallery extra caps there rather than letting the next release quietly produce no downloads.
  • sphinx-gallery's own .py/.zip downloads still end up copied into _downloads/, even though nothing on the page links to them anymore. Its .ipynb is never written: with gallery_downloads on, this extension sets sphinx_gallery_conf['pypandoc'] to False and sphinx_gallery_conf['notebook_extensions'] to an empty set before sphinx-gallery reads them, so pandoc is not needed. Setting pypandoc, notebook_extensions, first_notebook_cell, last_notebook_cell or notebook_images yourself has no effect and is reported as a warning.

Auditing the downloads

sphinx-examples-as-code-audit runs every download of a built HTML tree in the documentation's own environment, with plotting off screen, and reports what is wrong with each. The audit extra installs what it needs to run notebooks:

pip install 'sphinx-examples-as-code[audit]'
sphinx-examples-as-code-audit run _build/html/_downloads --html _build/html --format python --output python.jsonl
sphinx-examples-as-code-audit run _build/html/_downloads --html _build/html --format jupyter --output jupyter.jsonl
sphinx-examples-as-code-audit run _build/html/_downloads --html _build/html --format marimo --output marimo.jsonl
sphinx-examples-as-code-audit pages _build/html --output pages.jsonl
sphinx-examples-as-code-audit report *.jsonl --output report.md

With --html, only downloads a page links to are run. Each finding has a kind:

Kind Finding
syntax Code that is not valid Python, such as a doctest prompt or a % line
magic A line the marimo converter turned into an IPython magic
marimo-check marimo check fails, e.g. a name defined in two cells
dependency A script fails without a module its dependencies do not install
requires-python A download's requires-python excludes the Python running the audit
collapsed A doctest value is not the last statement of its notebook cell, so it is not shown
dropped-output A script does not print a doctest value
mismatch A notebook shows a value that differs from the doctest's expected output while the script's matches
python-mismatch The script's value differs too
marimo-rename marimo renamed a name to a private _name and the cell raises NameError
error A notebook fails while the script of the same example runs
python-error The script fails too
timeout A download stage runs longer than --timeout seconds
missing-download A link group or gallery page lacks a format other pages have
no-downloads No download of the format exists at all
  • Scripts run one top-level statement at a time. When a script looks up a module that only distributions outside the dependencies of its inline metadata provide, either itself or through a library where importing those dependencies alone does not, the script runs again with that module hidden, as in the environment uv run builds, and fails the check if it then fails. Notebooks are not checked for dependencies.
  • Downloads run in the current environment and user account. Pass --exclude with glob patterns of example names to skip any that delete or overwrite files, e.g. a cache.
  • Expected outputs come from the doctest blocks of each download's example, compared with ELLIPSIS, NORMALIZE_WHITESPACE and pytest's NUMBER. Pass --link-position bottom when link_position is 'bottom'.
  • report lists findings by kind and fails on all of them but python-mismatch and python-error, which the documentation build itself covers; pass --allow to choose.

The reusable workflow .github/workflows/audit-downloads.yml runs this on a docs artifact of the calling repository, or of repository, one job per format, and can file the report as an issue. Each job runs install in a checkout of the docs repository, at ref if given, else at the calling workflow's ref when repository is empty and at its default branch, then installs the audit from the workflow's own commit. With build, a job first builds the docs in that checkout with the extension at the workflow's own commit, and the audit runs that build:

jobs:
  audit:
    uses: pyvista/sphinx-examples-as-code/.github/workflows/audit-downloads.yml@v0.12.0
    permissions:
      actions: read
      contents: read
      issues: write
    with:
      artifact: docs-html
      install: uv pip install . --group docs
      headless-display: true
      issue-label: docs-download-failure

Development

uv sync --group dev --extra gallery
uv run pytest
uv run pre-commit run --all-files

Metadata

Release files for sphinx-examples-as-code 0.13.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sphinx-examples-as-code 0.13.0
File Size Uploaded Provenance Actions
sphinx_examples_as_code-0.13.0.tar.gz 118.6 kB With provenance Details for sphinx_examples_as_code-0.13.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for sphinx-examples-as-code 0.13.0
File Interpreter ABI Platform Actions
sphinx_examples_as_code-0.13.0-py3-none-any.whl Python 3 none any Details for sphinx_examples_as_code-0.13.0-py3-none-any.whl

Total release size: 168.6 kB

Release files / sphinx_examples_as_code-0.13.0.tar.gz

Download URL sphinx_examples_as_code-0.13.0.tar.gz
Size 118.6 kB
Tags Source
SHA-256 checksum
How to use checksums
9a37d291614aa5c22ee8f44d0476c7ad1f3a869274b5bcaf8c301446818771c8
BLAKE2b-256 checksum
How to use checksums
cfe3b9ac6040b63be9dfd7a3540adf9f4b734ba6df54d9df13a10db6f791cd1e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Release files / sphinx_examples_as_code-0.13.0-py3-none-any.whl

Download URL sphinx_examples_as_code-0.13.0-py3-none-any.whl
Size 50.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8dd4b98f5487498f1e2579d4f0621d11c7fe650986a3eb0a2a3535256169066d
BLAKE2b-256 checksum
How to use checksums
a08d8aba36a8699d1494113cc178815fe126aeb26d73edd98cef81af7013bf0e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

Security for sphinx-examples-as-code 0.13.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for sphinx_examples_as_code-0.13.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page