Alright, let’s talk about MkDocs. You’ve probably hit the wall with Sphinx and reStructuredText by now. It feels like building a cathedral when you just wanted a nice toolshed. MkDocs is the antidote: it’s documentation for the 99%. It understands that you, a developer, already live and breathe Markdown. It’s built on Python-Markdown, so your existing muscle memory for things like **bold** and code fences works perfectly. The whole thing is configured via a single, sane YAML file, and it generates a clean, searchable static site. It’s the IKEA of doc tools—you get modern, functional docs without needing a degree in technical writing to assemble them.

The real magic, however, isn’t MkDocs itself; it’s the plugins. Specifically, mkdocstrings. This is the plugin that automatically pulls docstrings from your code and injects them directly into your Markdown pages. It’s the reason you don’t have to choose between beautifully formatted prose and comprehensive API documentation.

The Basic Setup: mkdocs.yml

Your configuration file is the brain of the operation. Here’s a minimal, functional starting point that enables mkdocstrings. Notice we’re using the “material” theme here—it’s the undisputed champion for MkDocs, but you can use any theme you like.

site_name: My Awesome API
theme:
  name: "material"

plugins:
  - search
  - mkdocstrings

markdown_extensions:
  - admonition
  - pymdownx.highlight:
      anchor_linenums: true

The mkdocstrings plugin does the heavy lifting. The search plugin is non-negotiable for a usable site, and those markdown extensions give you nice things like highlighted code blocks and pretty note boxes.

The Magic Incantation: Automatic Docstrings

Now, the main event. In your Markdown files (e.g., docs/reference.md), you don’t write about your functions and classes—you simply summon them. mkdocstrings uses a special syntax to find the Python object and render its docstring.

Here’s how you call a specific class into existence on the page:

Here's the documentation for our core parser:

::: src.mypackage.parser.Parser

Yes, that’s it. The ::: followed by the dotted import path is the trigger. The plugin will find that Parser class, parse its docstring (hopefully you wrote a good one!), and render it into a beautiful, sectioned documentation block. It handles parameters, return types, raises clauses, attributes—everything. It feels like witchcraft the first time you see it.

Choosing and Configuring a Handler

Under the hood, mkdocstrings uses “handlers” for different languages. For Python, you have two excellent choices: python (the newer, cooler kid) and griffe (an evolution of the former, now the default and recommended choice). You configure the handler in your mkdocs.yml to control how the documentation is rendered.

This is where you fix the most common pitfall: the dreaded “No module named…” error. You must tell mkdocstrings where to find your code. The paths setting is your best friend.

plugins:
  - mkdocstrings:
      handlers:
        python:
          paths: [src] # Critical! Points to your source directory.
          options:
            show_source: true # Shows a link to the source code, brilliant for debugging.
            docstring_style: google # Supports 'google', 'numpy', 'sphinx'. Use what you've got.
            show_signature_annotations: false # Cleaner signatures, usually.

Setting paths: [src] is often the difference between glorious, automated docs and a frustrating error message. It prepends src/ to the module path, so it can find src.mypackage.parser correctly.

Cross-Referencing Like a Pro

What good is documentation if you can’t link between pages? mkdocstrings integrates seamlessly with the mkdocs-gen-files and mkdocs-literate-nav plugins to auto-generate a full API reference section. But for manual cross-references, you use Markdown.

To link to another Markdown page, just use a regular link: [Writing Overview](writing.md).

To link to a specific Python object that mkdocstrings knows about, you use its special cross-reference syntax. This is a killer feature.

The `Parser` class is the heart of the system. For more details, see the [Parser][src.mypackage.parser.Parser] class itself, or its primary method, [parse()][src.mypackage.parser.Parser.parse].

The plugin will automatically generate the correct HTML link for you. It’s airtight.

The One Big Gotcha: Relative Imports

Here’s the trap that gets everyone. Your docstrings are parsed in the context of your package. If you use relative imports inside a docstring (e.g., :param MyClass obj: Description), the autodoc machinery can get horribly confused and fail. The solution is almost always to use the absolute path within your docstrings (:param mypackage.main.MyClass obj:). It’s uglier, but it’s unambiguous and always works. It’s one of those places where the designers chose correctness over convenience, and while I get it, it’s still a pain.

MkDocs with mkdocstrings is the pragmatic choice. It doesn’t have every single bell and whistle of Sphinx, but it gets you 95% of the way there with 10% of the effort. It respects your time, which is the highest compliment a tool can be paid. Now go make your docs something you’d actually want to read.