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

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.

flowchart LR SRC["Python sources
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

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

docstring_style

"google"

Parser for the docstrings: auto, google, numpy, sphinx, or None to render them as plain text.

docstring_section_style

"table"

How the Parameters, Returns and Raises sections are drawn: table, list or spacy.

heading_level

2

Level of the first heading generated for a marker; use 1 on a page that has no title of its own.

show_root_heading

false

Show a heading for the object named after ::: itself. Off by default, so a marker for one function renders no title.

show_source

true

Include a collapsible block with the source code of each object.

show_signature_annotations

false

Put type annotations inside the signature; the types are still shown in the parameter table.

separate_signature

false

Draw the whole signature in a code block below the heading.

members_order

"alphabetical"

alphabetical, source, or __all__ to follow the declared export order.

show_submodules

false

Render the submodules of a package recursively.

inherited_members

false

Include members inherited from base classes.

merge_init_into_class

false

Fold the __init__ documentation into the class entry.

filters

["!^_[^_]"]

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.

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"

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

autoapi_dir

"."

Directory to scan, relative to zensical.toml; the build fails with "autoapi_dir is not a directory" if it does not exist.

autoapi_file_patterns

["*.py", "*.pyi"]

File name patterns that count as sources.

autoapi_ignore

[]

Patterns to leave out.

autoapi_root

"autoapi"

Output subdirectory of the docs; must be a non-empty relative path, and ., ./api, absolute paths and .. are rejected.

autoapi_add_nav_entry

true

Add the generated pages to the navigation; a string is used as the section title.

autoapi_generate_api_docs

true

Generate the pages; set it to false to use the plugin only for discovery.

autoapi_keep_files

false

Keep the generated Markdown (summary.md and one .md per module) in the docs directory instead of removing it.

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

modules

[]

Paths of the Python packages or modules to document, relative to zensical.toml.

module_options

{}

Per-module handler options, as a table of pattern to options.

exclude

[]

Modules to skip; prefix a regular expression with re:.

nav_section_title

"API Reference"

Title of the navigation section.

api_root_uri

"reference"

Output subdirectory of the docs; same restrictions as autoapi_root.

exclude_private

true

Skip modules whose name starts with an underscore.

show_full_namespace

false

Label navigation items with the full dotted name instead of the last part.

nav_item_prefix

a module icon

HTML placed before each navigation item.

on_implicit_namespace_package

"warn"

What to do with a directory that lacks __init__.py: raise, warn or skip.

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

mkdocstrings: pocket_units.missing could not be found, then Could not collect 'pocket_units.missing'

The identifier after ::: is not importable from the configured paths. The build stops with a Python traceback after the warning. Fix the name, or add the directory holding the package to paths.

unknown api-autonav option: bogus

A setting that the plugin does not have. Misspelled keys of the supported plugins are rejected at configuration time.

api-autonav api_root_uri must be a relative directory path

The root is empty, ., absolute or contains ... The same rule applies to autoapi_root.

api-autonav module is not a Python file or directory: <path>

An entry of modules does not exist below the project directory.

Docstring sections printed as literal text

docstring_style does not match the style of the docstrings; no error is raised.

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-files is not supported; the autoapi and api-autonav generators replace its usual use for API pages.

  • Both generators were added in Zensical 0.0.66, mkdocstrings in 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 --clean after an upgrade, because generated pages and cache files can survive between runs.