Python MkDocs Documentation — Deep Dive
Versioned Documentation with mike
When your library maintains multiple versions, users need docs that match their installed version. The mike tool manages versioned MkDocs deployments:
pip install mike
# Deploy version 2.0
mike deploy 2.0 latest --push --update-aliases
# Deploy version 1.9 (still accessible but not default)
mike deploy 1.9 --push
Each version lives in its own subdirectory on GitHub Pages. A version selector dropdown appears in the header, letting users switch between releases. The latest alias always points to the newest stable version.
Integrating mike with CI
# .github/workflows/docs.yml
on:
push:
tags: ["v*"]
jobs:
deploy-docs:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # mike needs full history
- uses: actions/setup-python@v5
- run: pip install mkdocs-material mike mkdocstrings[python]
- run: |
git config user.name "docs-bot"
git config user.email "docs@example.com"
VERSION=${GITHUB_REF#refs/tags/v}
mike deploy $VERSION latest --push --update-aliases
mike set-default latest --push
Plugin Architecture
MkDocs plugins are Python classes that hook into the build lifecycle:
from mkdocs.plugins import BasePlugin
from mkdocs.config import config_options
class WordCountPlugin(BasePlugin):
config_scheme = (
("min_words", config_options.Type(int, default=100)),
)
def on_page_markdown(self, markdown, page, config, files):
word_count = len(markdown.split())
if word_count < self.config["min_words"]:
print(f"WARNING: {page.file.src_path} has only {word_count} words")
return markdown
def on_page_context(self, context, page, config, nav):
context["word_count"] = len(page.markdown.split())
return context
Register in setup.py:
entry_points={
"mkdocs.plugins": [
"wordcount = my_plugin:WordCountPlugin",
]
}
Key lifecycle hooks:
| Hook | Timing | Use Case |
|---|---|---|
on_config | After config loaded | Validate settings, modify config |
on_files | After file collection | Add/remove/modify file list |
on_page_markdown | Before Markdown rendering | Transform content |
on_page_context | Before template rendering | Inject template variables |
on_post_build | After build complete | Generate sitemaps, reports |
Macros Plugin: Dynamic Content
The mkdocs-macros-plugin brings Jinja2 templating to your Markdown:
plugins:
- macros:
module_name: docs/macros
# docs/macros.py
import subprocess
def define_env(env):
@env.macro
def python_version():
return subprocess.check_output(
["python", "--version"], text=True
).strip()
@env.macro
def include_output(command):
return subprocess.check_output(
command, shell=True, text=True
)
Then in Markdown:
Built with {{ python_version() }}
### CLI Help Output
```text
{{ include_output("python -m mypackage --help") }}
```
This ensures CLI help text, version numbers, and generated tables are always current at build time.
mkdocstrings Advanced Configuration
Fine-tune API documentation rendering:
plugins:
- mkdocstrings:
default_handler: python
handlers:
python:
paths: [src]
options:
docstring_style: google
docstring_options:
ignore_init_summary: true
merge_init_into_class: true
show_root_heading: true
show_source: true
show_bases: true
members_order: source
filters:
- "!^_" # exclude private
- "^__init__$" # but include __init__
Auto-Generated API Pages
Use mkdocs-gen-files and mkdocs-literate-nav to auto-generate one page per module:
# docs/gen_ref_pages.py
"""Generate API reference pages."""
from pathlib import Path
import mkdocs_gen_files
nav = mkdocs_gen_files.Nav()
src = Path("src")
for path in sorted(src.rglob("*.py")):
module_path = path.relative_to(src).with_suffix("")
doc_path = path.relative_to(src).with_suffix(".md")
full_doc_path = Path("api", doc_path)
parts = tuple(module_path.parts)
if parts[-1] == "__init__":
parts = parts[:-1]
doc_path = doc_path.with_name("index.md")
full_doc_path = full_doc_path.with_name("index.md")
elif parts[-1].startswith("_"):
continue
nav[parts] = doc_path.as_posix()
with mkdocs_gen_files.open(full_doc_path, "w") as fd:
identifier = ".".join(parts)
fd.write(f"::: {identifier}")
mkdocs_gen_files.set_edit_path(full_doc_path, path)
with mkdocs_gen_files.open("api/SUMMARY.md", "w") as nav_file:
nav_file.writelines(nav.build_literate_nav())
plugins:
- gen-files:
scripts:
- docs/gen_ref_pages.py
- literate-nav:
nav_file: SUMMARY.md
- mkdocstrings
This creates a full API reference tree that stays synchronized with your package structure — no manual page creation needed.
Multi-Repo Documentation
For organizations with multiple repositories that need unified documentation, the monorepo plugin aggregates docs from different sources:
plugins:
- monorepo
nav:
- Home: index.md
- Auth Service: '!include ./services/auth/mkdocs.yml'
- Payment Service: '!include ./services/payment/mkdocs.yml'
Each service maintains its own mkdocs.yml and docs/ directory. The parent project stitches them together into a single site with unified search and navigation.
Performance Optimization
For large documentation sites (500+ pages):
plugins:
- search:
separator: '[\s\-\.\_]+' # better tokenization
- social:
cards_layout: default # pre-generate social cards
- offline:
enabled: !ENV [OFFLINE, false]
Build optimizations:
# Use strict mode to catch issues early
mkdocs build --strict
# Profile slow builds
mkdocs build --verbose 2>&1 | grep -i "time"
The biggest performance killer is usually mkdocstrings importing heavy packages. Use autodoc_mock_imports-style tricks by configuring lightweight stubs in your docs environment.
MkDocs vs. Sphinx: Decision Framework
| Factor | Choose MkDocs | Choose Sphinx |
|---|---|---|
| Team skill | Knows Markdown | Knows reStructuredText |
| Content type | Guides, tutorials | Dense API reference |
| Setup time | 5 minutes | 30 minutes |
| Theme quality | Material is exceptional | Read the Docs is functional |
| Versioning | mike (simple) | sphinx-multiversion (flexible) |
| Cross-references | Basic | Excellent (domains, roles) |
| Build speed | Fast | Slower but parallelizable |
Many mature projects use both: MkDocs for the user-facing documentation site, and Sphinx (published to a subdomain) for the exhaustive API reference.
One thing to remember: MkDocs with Material, mkdocstrings, and mike gives you a documentation platform that rivals commercial products — versioned, searchable, auto-generated API docs, all from Markdown files living next to your code.
See Also
- Python Black Formatter Understand Black Formatter through a practical analogy so your Python decisions become faster and clearer.
- Python Bumpversion Release Change your software's version number in every file at once with a single command — no more find-and-replace mistakes.
- Python Changelog Automation Let your git commits write the changelog so you never forget what changed in a release.
- Python Ci Cd Python Understand CI CD Python through a practical analogy so your Python decisions become faster and clearer.
- Python Cicd Pipelines Use Python CI/CD pipelines to remove setup chaos so Python projects stay predictable for every teammate.