2025, Dec 24 17:00

Turn Python example scripts into narrative documentation with Sphinx-Gallery or nbsphinx and jupytext

Learn how to convert Python example scripts into narrative documentation using Sphinx-Gallery or nbsphinx with jupytext, keeping code runnable and docs in sync.

When you ship a Python library, example scripts often become the most practical entry point for users. The challenge: turning those executable scripts into narrative documentation without copy-pasting code into reStructuredText and without relying on extensions that only show comments as comments. If you tried to lean on Sphinx’s viewcode, you already know it renders source nicely but does not interpret comments as prose.

Problem example

Here’s a minimal script that needs narrative-style documentation while remaining runnable in the repository:

#: Import necessary package and define :meth:`build_bins`
import numpy as np
def build_bins(lo, hi):
    """
    Make a grid for constant by piece functions
    """
    t = np.linspace(0, np.pi)
    t_mid = (t[:-1] + t[1:]) / 2
    widths = t[1:] - t[:-1]
    return t_mid, widths
#: Interpolate a function
centers, steps = build_bins(0, np.pi)
vals = np.sin(centers)
#: Calculate its integral
acc = np.sum(vals * steps)
print("Result %g" % acc)

Why plain Sphinx falls short here

The desired output is a narrative page where explanatory text sits alongside code cells and results. However, the default tooling that displays source, such as viewcode, does not promote inline comments to formatted text. It presents them verbatim as part of the code block. To convert a script into a story—text paragraphs interleaved with executable code—you need tooling designed for that pattern.

Solution: sphinx-gallery

The sphinx-gallery extension generates narrative documentation pages directly from Python scripts. It expects a header docstring and uses # %% sections to distinguish prose from code. Text sections render as documentation, while code blocks remain executable during the build if you enable execution.

A script adapted to this workflow can look like this:

"""
My example script.
"""
import numpy as np
# %%
# This will be a text block
def build_bins(lo, hi):
    """
    Make a grid for constant by piece functions
    """
    t = np.linspace(0, np.pi)
    t_mid = (t[:-1] + t[1:]) / 2
    widths = t[1:] - t[:-1]
    return t_mid, widths
# %%
# Another block of text
centers, steps = build_bins(0, np.pi)
vals = np.sin(centers)
acc = np.sum(vals * steps)
print("Result %g" % acc)

With this structure, sphinx-gallery formats the # %% regions that contain comments as narrative text, while keeping the rest as code cells in the generated HTML. The extension provides the gallery page linking to your examples and the interleaved text-and-code rendering for each script page.

Alternative: nbsphinx + jupytext

If a thumbnail gallery page is not what you want, another option is to generate documentation pages via nbsphinx and jupytext. You can keep your examples as plain .py files in jupytext’s percent format and let Sphinx convert them into notebooks during the build. The project layout can, for instance, link your example scripts into the docs directory:

.
├── docs
│   ├── Makefile
│   ├── conf.py
│   ├── examples -> ../src/examples/
│   ├── index.rst
│   └── make.bat
└── src
    └── examples
        └── narrative.py

In conf.py, add nbsphinx and configure jupytext conversion for percent-format Python files:

# add nbsphinx to extensions
extensions = [
  ...
  "nbsphinx",
]
# this converts .py files with the percent format to notebooks
nbsphinx_custom_formats = {
  '.py': ['jupytext.reads', {'fmt': 'py:percent'}],
}
nbsphinx_output_prompt = ""
nbsphinx_execute = "auto"
templates_path = ['_templates']
# add conf.py to exclude_patterns
exclude_patterns = [..., 'conf.py']

The example script in percent format can then be written as follows. Text can be Markdown cells, and you can also include raw reStructuredText where needed:

# %% [markdown]
# # A title
# %% [raw] raw_mimetype="text/restructuredtext"
# Import necessary package and define :meth:`build_bins`
# %%
import numpy as np
def build_bins(lo, hi):
    """
    Make a grid for constant by piece functions
    """
    t = np.linspace(0, np.pi)
    t_mid = (t[:-1] + t[1:]) / 2
    widths = t[1:] - t[:-1]
    return t_mid, widths
# %% [markdown]
# Interpolate a function
# %%
centers, steps = build_bins(0, np.pi)
vals = np.sin(centers)
# %% [markdown]
# Calculate its integral
# %%
acc = np.sum(vals * steps)
print("Result %g" % acc)

Building the docs produces an HTML page for the script, which you can link from index.rst or elsewhere in your documentation.

When using this approach, ensure the Python file starts with a title cell so the resulting page can be referenced from other documents. Text cells marked as Markdown are interpreted as such, and raw cells with raw_mimetype="text/restructuredtext" are treated as reStructuredText. By default, input prompts like [1]: appear for code cells; disabling them involves custom CSS.

Why this matters

Both approaches let you maintain a single source of truth: your example scripts stay runnable in the repository while also powering the narrative documentation. That prevents divergence between docs and code, helps users reproduce examples exactly as published, and reduces maintenance noise from duplicated snippets.

Closing notes

Pick sphinx-gallery if you want a gallery-style entry point and straightforward script-to-page conversion with # %% sections. Pick nbsphinx with jupytext if you prefer notebook-style pages without a gallery, while keeping scripts in percent format. There is also a different angle entirely—writing documentation as the source and generating code from it—where a literate programming approach can be helpful.

Whichever path you adopt, keep the scripts close to how users will write their own code, start pages with a clear title, and structure text and cells consistently so the build tooling can do the heavy lifting.