API Docs with mkdocstrings
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against Zensical 0.0.72 (the latest release on PyPI, published on 2026-10-23), mkdocstrings 1.0.8 and mkdocstrings-python 2.0.10, and checked on 2026-10-26. Zensical is still an alpha-stage 0.0.x project, so option names and behaviour can change between releases. Every example below was built with that combination in a scratch project.
What mkdocstrings does and why Zensical supports it
mkdocstrings turns the documentation comments of source code into pages of a documentation site. You write a one-line
marker such as ::: pocket_units in a Markdown page, and the build replaces it with the rendered documentation of that
Python module: signatures, type annotations, the sections of each docstring, and anchors that other pages can link to.
The Python support lives in a separate package, mkdocstrings-python, which reads the source with Griffe, a library
that analyses Python code statically (without importing it) and can fall back to inspecting the live object.
Zensical implements the mkdocstrings plugin natively: it is one of the plugins listed as supported since Zensical
0.0.11, and its settings sit in zensical.toml under [project.plugins.mkdocstrings]. The handler that does the
real work for Python is still the ordinary Python package, so it has to be installed next to Zensical:
python3 -m venv .venv
source .venv/bin/activate
pip install zensical==0.0.72 mkdocstrings-python==2.0.10
Source: Zensical compatibility: plugins
The published settings of the plugin are enabled (default true), default_handler (default python), handlers
(default empty), custom_templates, enable_inventory and locale (default en). Two differences from the MkDocs
plugin matter in practice. The watch setting is ignored, and sources outside the project directory are not watched
during preview, so a library kept in a sibling directory is not rebuilt when you edit it. Unknown settings of a
supported plugin raise a configuration error instead of being ignored silently, whereas the configuration of a plugin
Zensical does not know at all is discarded without a message.
The running example
The Pocket Units library has a Python package with one module and a package __init__. The docstrings use the Google
style, which is the default of mkdocstrings-python. The full Python docstring conventions (Google, NumPy, reStructuredText)
are described on Python docstrings; this page does not repeat them and
only shows how to tell the handler which one you used.
"""Conversion between length units."""
from enum import Enum
class LengthUnit(Enum):
"""Length units supported by :func:`convert`."""
METRE = 1.0
KILOMETRE = 1000.0
FOOT = 0.3048
MILE = 1609.344
def convert(value: float, from_unit: LengthUnit, to_unit: LengthUnit) -> float:
"""Convert a length between two units.
Args:
value: The magnitude to convert.
from_unit: The unit of ``value``.
to_unit: The target unit.
Returns:
The converted magnitude.
Raises:
TypeError: If a unit is not a :class:`LengthUnit`.
Example:
>>> round(convert(10, LengthUnit.KILOMETRE, LengthUnit.MILE), 3)
6.214
"""
if not isinstance(from_unit, LengthUnit) or not isinstance(to_unit, LengthUnit):
raise TypeError("units must be LengthUnit members")
return value * from_unit.value / to_unit.value
The file above is pocket_units/converter.py. The package pocket_units/__init__.py re-exports the public names with
__all__ = ["LengthUnit", "convert"].
Three ways to produce API pages
The figure shows the whole flow. Python sources are read by Griffe through the handler. A page contains one or more
::: markers, and you obtain those pages in one of three ways: write them by hand, let the autoapi generator create
one page per module, or let the api-autonav generator create the pages and the navigation from a list of modules.
pocket_units/*.py"] --> GR["Griffe
static analysis"] GR --> H["mkdocstrings-python
handler"] MAN["Handwritten page
with ::: marker"] --> H AA["autoapi
one page per module"] --> H AN["api-autonav
pages and navigation"] --> H H --> HTML["Rendered API pages
site/**/index.html"] H --> INV["objects.inv
cross-reference index"]
Figure: from Python sources to API pages and the cross-reference inventory.
Handwritten pages
The simplest setup is a Markdown page that names the object to document. Create docs/api.md:
# API reference
::: pocket_units
and configure the handler in zensical.toml, telling it where to find the package. paths lists directories added to
the search path of Griffe, relative to the directory that holds zensical.toml:
[project]
site_name = "Pocket Units"
site_url = "https://example.org/pocket-units/"
nav = [{ "Home" = "index.md" }, { "API reference" = "api.md" }]
[project.plugins.mkdocstrings.handlers.python]
paths = ["."]
[project.plugins.mkdocstrings.handlers.python.options]
docstring_style = "google"
show_source = false
members_order = "source"
Running python -m zensical build --clean on this project printed:
Build started
No issues found
Build finished in 0.44s
The page site/api/index.html then contains a heading for each public name (pocket_units.LengthUnit and
pocket_units.convert), the signature convert(value, from_unit, to_unit), the base class Enum, and one table each
for parameters (name, type, description, default) and raised exceptions, the Returns section, and the doctest from the
Example section. The site also gets an objects.inv file in Sphinx inventory format, which other projects can use to
link to your API.
The identifier after ::: is an import path: pocket_units documents the package, pocket_units.converter the module,
pocket_units.converter.convert a single function. Options can also be given for one marker only, in YAML indented below
it. Local options override the ones in zensical.toml:
::: pocket_units.converter
options:
show_root_heading: true
members:
- convert
Source: mkdocstrings: Usage
Handler options worth knowing
The handler options go under [project.plugins.mkdocstrings.handlers.python.options]. The defaults below were read from
the installed mkdocstrings-python 2.0.10.
| Option | Default | Effect |
|---|---|---|
|
|
Parser for the docstrings: |
|
|
How the Parameters, Returns and Raises sections are drawn: |
|
|
Level of the first heading generated for a marker; use |
|
|
Show a heading for the object named after |
|
|
Include a collapsible block with the source code of each object. |
|
|
Put type annotations inside the signature; the types are still shown in the parameter table. |
|
|
Draw the whole signature in a code block below the heading. |
|
|
|
|
|
Render the submodules of a package recursively. |
|
|
Include members inherited from base classes. |
|
|
Fold the |
|
|
Regular expressions selecting members; the default hides names that start with one underscore but keeps dunder names. |
A full list, with the options of the signature, member, heading and docstring groups, is in the handler documentation. The options are Python handler options, so they work the same in Zensical and in MkDocs.
Choosing the docstring style
The handler does not guess your style unless you ask for auto: it parses every docstring with docstring_style, which
defaults to Google. With the Pocket Units sources set to numpy the build still finishes with "No issues found", but the
Args:, Returns: and Raises: headings are no longer recognised. They appear as literal text in the page and the
parameter table is missing. A wrong style therefore shows up as unformatted output, not as an error. Match
docstring_style to the style your code uses, or use auto for a code base that mixes styles. Sphinx field lists
(:param x:) are selected with sphinx. The choices and their syntax are explained on the
Python docstrings page.
Docstring text is parsed as Markdown, not as reStructuredText. The role :func:`convert` in the class docstring above is
therefore printed as plain text in the rendered page. To link to another object, use the cross-reference syntax of
Markdown described in the next section, for example [convert][pocket_units.convert].
Cross-references between pages and objects
mkdocstrings registers each documented object with an identifier, which is its import path, and the autorefs machinery
resolves a link written as [text][identifier] on any page of the site. Pocket Units docs/index.md can link to the
function without knowing the URL of the API page:
# Pocket Units
Use [convert][pocket_units.convert] to change a length from one unit to another.
In the scratch build the link rendered as api/#pocket_units.convert. When the same name is exported from two places,
as convert is in pocket_units and pocket_units.converter, write the identifier of the one you want; each has its own
anchor. The plugin setting enable_inventory relates to the objects.inv file that other sites use to link to yours; the
Zensical documentation lists it without further description.
Source: mkdocstrings: Cross-references
Generating the pages: autoapi
Writing a marker page for every module becomes tedious. Zensical offers two built-in generators that do it from the
source tree, so that the MkDocs gen-files plugin, which Zensical does not support, is not needed. Both are
implemented natively and exist since Zensical 0.0.66, and both need the mkdocstrings handler to be the Python one.
The autoapi generator corresponds to the MkDocs plugin mkdocs-autoapi, which is also the name the plugin goes by in
the configuration. It walks a directory, finds source files and writes one page per module plus a navigation entry.
[project]
site_name = "Pocket Units"
site_url = "https://example.org/pocket-units/"
[project.plugins.mkdocstrings.handlers.python]
paths = ["."]
[project.plugins.mkdocstrings.handlers.python.options]
docstring_style = "google"
show_source = false
[project.plugins.mkdocs-autoapi]
autoapi_dir = "pocket_units"
autoapi_root = "api"
autoapi_add_nav_entry = "API reference"
Source: Zensical compatibility: plugins and mkdocs-autoapi: Usage
With docs/index.md as the only handwritten page, the build wrote these pages and a navigation section titled "API
reference" holding pocket_units (with an Index page) and converter:
site/index.html
site/api/pocket_units/index.html
site/api/pocket_units/converter/index.html
Each generated page contains one marker, ::: pocket_units and ::: pocket_units.converter. The settings are:
| Setting | Default | Meaning |
|---|---|---|
|
|
Directory to scan, relative to |
|
|
File name patterns that count as sources. |
|
|
Patterns to leave out. |
|
|
Output subdirectory of the docs; must be a non-empty relative path, and |
|
|
Add the generated pages to the navigation; a string is used as the section title. |
|
|
Generate the pages; set it to false to use the plugin only for discovery. |
|
|
Keep the generated Markdown ( |
Discovery skips symbolic links below the scanned directory and always excludes .git, .venv, venv, __pycache__
and the output and cache directories, so the default scan of the project root in a project with a virtual environment does
not pick up third-party code. The generated files exist only during the build unless autoapi_keep_files is true. With
it set, the Pocket Units project ended up with docs/api/summary.md, listing pocket_units and converter as a nested
Markdown list, plus docs/api/pocket_units/index.md and converter.md containing the single marker lines. Commit them
only if you want to hand-edit the pages afterwards.
Generating the pages: api-autonav
api-autonav corresponds to the MkDocs plugin of the same name. Where autoapi scans a directory, api-autonav is given a
list of modules, builds the navigation from the package structure, and can hide private modules.
[project]
site_name = "Pocket Units"
site_url = "https://example.org/pocket-units/"
[project.plugins.mkdocstrings.handlers.python]
paths = ["."]
[project.plugins.mkdocstrings.handlers.python.options]
docstring_style = "google"
show_source = false
[project.plugins.api-autonav]
modules = ["pocket_units"]
nav_section_title = "API reference"
api_root_uri = "reference"
exclude_private = true
show_full_namespace = false
This build produced site/reference/pocket_units/index.html and site/reference/pocket_units/converter/index.html,
with a navigation section "API reference" holding pocket_units. The settings are:
| Setting | Default | Meaning |
|---|---|---|
|
|
Paths of the Python packages or modules to document, relative to |
|
|
Per-module handler options, as a table of pattern to options. |
|
|
Modules to skip; prefix a regular expression with |
|
|
Title of the navigation section. |
|
|
Output subdirectory of the docs; same restrictions as |
|
|
Skip modules whose name starts with an underscore. |
|
|
Label navigation items with the full dotted name instead of the last part. |
|
a module icon |
HTML placed before each navigation item. |
|
|
What to do with a directory that lacks |
Choose between the two like this. Use autoapi when the layout is "everything in this directory" and you want little
configuration. Use api-autonav when you want to name the public packages, hide private modules, or set handler options
per module. Enabling both for the same modules would generate two sets of pages, so pick one per package. Zensical still reads
mkdocs.yml for projects that have not moved to TOML, where the same plugins are configured under plugins: with the
names mkdocs-autoapi and api-autonav.
Common errors
| Message | Cause |
|---|---|
|
The identifier after |
|
A setting that the plugin does not have. Misspelled keys of the supported plugins are rejected at configuration time. |
|
The root is empty, |
|
An entry of |
Docstring sections printed as literal text |
|
The messages above were produced in the scratch project. If the handler package is missing altogether, Zensical cannot
render the markers; install mkdocstrings-python into the same environment as Zensical. That case was not reproduced
here.
Pitfalls and version notes
-
gen-filesis not supported; theautoapiandapi-autonavgenerators replace its usual use for API pages. -
Both generators were added in Zensical 0.0.66,
mkdocstringsin 0.0.11. On an older release they are not available. -
Sources outside the project directory are not watched by
zensical serve, so changes there do not trigger a rebuild. -
Griffe analyses sources statically by default. Code that only exists at runtime (attributes created dynamically) may need the handler options for inspection; the options are described in the handler documentation.
-
Keep the page-level options small and the global ones in
zensical.toml, so that every API page looks the same. -
Regenerate with
zensical build --cleanafter an upgrade, because generated pages and cache files can survive between runs.