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.
pip install sphinx-examples-as-codeSphinx-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',
]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,
}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 aformatsthat names'marimo'(inconf.pyor 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_showforpyvista.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 toNoneto 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 astext urlin 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:Trueadds 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 withscript_metadataon.requires_python: arequires-pythonspecifier for the dependency block, e.g.'>=3.10'.None(default) derives one from the dependencies instead; see Dependencies and run commands. Only used withscript_metadataon.
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.
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
# /// scriptblock, 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--pythononly when there is arequires-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
# /// scriptblock, which also listsmarimo, 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 for a single page in an examples-as-code-dependencies directive anywhere on it.
It takes PEP 508 requirements separated by spaces or commas, adds them to every download from
that page, and renders nothing:
.. examples-as-code-dependencies:: sympy imageio-ffmpeg>=0.4In a Sphinx-Gallery example, put it in the example's docstring. 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, one minor version above the newest Python named in theProgramming Language :: Python :: 3.Xclassifiers of that project and of every dependency, listed or left out, installed from a platform-specific wheel, as installed in the environment building the docs. Pure-Python dependencies and dependencies without such classifiers don't count, and the bound is dropped when it would exclude every Python the project's ownRequires-Pythonallows.
With neither, the line is left out.
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 — only the input code matters. .. code-block:: python(orpy) blocks are kept as-is; 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`). Ifhtml_baseurlis 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 thegalleryextra caps there rather than letting the next release quietly produce no downloads. - sphinx-gallery's own
.py/.zipdownloads still end up copied into_downloads/, even though nothing on the page links to them anymore. Its.ipynbis never written: withgallery_downloadson, this extension setssphinx_gallery_conf['pypandoc']toFalseandsphinx_gallery_conf['notebook_extensions']to an empty set before sphinx-gallery reads them, so pandoc is not needed. Settingpypandoc,notebook_extensions,first_notebook_cell,last_notebook_cellornotebook_imagesyourself has no effect and is reported as a warning.
uv sync --group dev --extra gallery
uv run pytest
uv run pre-commit run --all-files