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:

HookTimingUse Case
on_configAfter config loadedValidate settings, modify config
on_filesAfter file collectionAdd/remove/modify file list
on_page_markdownBefore Markdown renderingTransform content
on_page_contextBefore template renderingInject template variables
on_post_buildAfter build completeGenerate 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

FactorChoose MkDocsChoose Sphinx
Team skillKnows MarkdownKnows reStructuredText
Content typeGuides, tutorialsDense API reference
Setup time5 minutes30 minutes
Theme qualityMaterial is exceptionalRead the Docs is functional
Versioningmike (simple)sphinx-multiversion (flexible)
Cross-referencesBasicExcellent (domains, roles)
Build speedFastSlower 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.

pythondocumentationdeveloper-tools

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.