Skip to content

Migrating from mkdocstrings

Both plugins render the same model. Griffe produces this model. mkdocstrings-python already runs Griffe as its static analyser, and starlight-pydocs runs Griffe the same way. Your docstrings, anchors and published objects.inv file carry over unchanged. Deep links into your documentation, and links from other projects into it, keep working.

This handler configuration:

mkdocs.yml
plugins:
- mkdocstrings:
handlers:
python:
# 1. paths -> search
paths: [src]
options:
# 2. docstring_style -> docstringStyle
docstring_style: google
# 3. show_source -> sourceLink
show_source: true
filters: ["!^_"]
# 4. inventories -> inventories
inventories:
- url: https://docs.python.org/3/objects.inv

becomes this, with the same four options at the matching numbers:

astro.config.mjs
import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import starlightPydocs, { pydocsSidebarGroup } from 'starlight-pydocs';
export default defineConfig({
integrations: [
starlight({
title: 'mypkg',
plugins: [
starlightPydocs({
packages: [
{
name: 'mypkg',
// 1. paths -> search
search: ['../src'],
// 2. docstring_style -> docstringStyle
docstringStyle: 'google',
// 3. show_source -> sourceLink
sourceLink: { host: 'github', repo: 'you/mypkg', ref: 'main', root: '..' },
},
],
// 4. inventories -> inventories
inventories: ['python'],
}),
],
sidebar: [{ label: 'API reference', items: [pydocsSidebarGroup] }],
}),
],
});

starlight-pydocs resolves search against the Astro project root, so a docs site in docs/ reaches its Python sources with ../src.

starlight-pydocs hides underscore-prefixed members by default, so filters: ["!^_"] needs no counterpart. Use filters: { private: true } for the opposite.

Each remaining handler option and its equivalent:

mkdocs.yml (plugins.mkdocstrings.handlers.python) starlight-pydocs
paths: [src] packages[].search: ['../src'] (relative to the Astro project)
docstring_style: numpy packages[].docstringStyle: 'numpy'
docstring_options: {…} packages[].docstringOptions: {…}
members: [A, B] / members: false packages[].members.include (globs on dotted paths)
filters: ["!^_"] packages[].filters plus packages[].members.exclude
inherited_members: true packages[].filters.inherited (already true)
show_source: true packages[].sourceLink (a forge preset or a template)
inventories: [{url: …}] top-level inventories: [{ url, base? }] or 'python'
extensions: [griffe_pydantic] packages[].extensions, plus extraRequirements
::: mypkg.Report in a page <Autodoc name="mypkg.Report" /> in an .mdx page
heading_level: 3 <Autodoc … headingLevel={3} />
paths + several handlers several entries in packages
mkdocstrings installed in the docs environment nothing: uv or a pre-generated dump
  • Docstrings: google, numpy and sphinx, parsed by the same Griffe parsers with the same options.
  • Anchors: #mypkg.report.Report.generate, the dotted object path, as mkdocstrings emits it.
  • __all__: a module that declares __all__ publishes that list. Without it, visibility heuristics apply, and filters and glob selection then refine the result. If an object is missing after the move, check __all__ or a filter first; the cause is rarely extraction.
  • objects.inv, in both directions: same names, roles and anchors. Other projects’ intersphinx_mapping entries keep resolving, and you can consume any inventory you consume today.
  • Cross-references in prose: [title][mypkg.thing] and [mypkg.thing][], resolved against your own package first, then the inventories.
  • __init__ merged into the class, inherited members with provenance, positional-only and keyword-only markers, property and classmethod badges.

starlight-pydocs generates pages; you do not place them by hand. mypkg is documented at /api/mypkg/, and each submodule at /api/mypkg/<module>/, nested to mirror the package. base moves the whole tree.

Pages are injected routes, not .md files written into src/content/docs. Markdown is a lossy format for an API reference: dict[str, float] | None needs escaping everywhere before you can link its parts, heading IDs go through a slugger instead of the dotted object path, and there is nowhere to attach symbol-level search metadata. Nothing is written to your content directory, and astro dev reflects a source change without rewriting files.

Members are documented on their module’s page, so mypkg.report.Report.generate is a heading on /api/mypkg/report/. This matches an mkdocstrings site with one ::: per module, the common layout.

There is no page-per-symbol mode. If your mkdocstrings site has one page per class, those URLs change. The anchors within the module page do not change.

starlight-pydocs identifies a package entry by its URL base. mkdocstrings has no such notion. The same import name can be documented at several bases, one per release. This is how versioned docs work.

Generated pages cover whole modules, so you only need ::: when you want one object inside your own prose. <Autodoc> does this: it renders the object exactly as the generated pages do, at whatever heading level you choose:

src/content/docs/reference.mdx
import { Autodoc } from 'starlight-pydocs/components';
<Autodoc name="mypkg.Report" headingLevel={3} />

When the same import name is documented at more than one base, <Autodoc package="…"> takes the base. See Autodoc.

Griffe runs as griffe dump. starlight-pydocs finds it through uv, or through an interpreter that already has it, so you do not need to install anything into a docs environment, unlike mkdocstrings. Griffe performs static analysis, so it never imports your package. You can also skip the subprocess entirely and commit a pre-generated dump. This is the route for CI without Python.

There are no mkdocs hooks and no plugin ecosystem: nothing maps to hooks.py, mkdocs-gen-files or mkdocs-literate-nav. Templates are Astro components, not Jinja. There are nine of them, with typed props.

What replaces them:

Most show_* options do not exist. Signatures, source links, badges, member summaries and inherited members always render. Use CSS and component overrides to change them.

starlight-pydocs does not show @typing.overload signatures yet. Griffe’s Function.as_dict does not serialise overloads, so the dump only contains the implementation. The renderer already stacks overload signatures when a dump carries them, so a future Griffe release will fix this.