Python pdoc API Docs — Deep Dive

pdoc’s Inspection Engine

Under the hood, pdoc imports your module and uses a combination of inspect, ast, and runtime introspection to build a documentation tree. Understanding this process helps you write docstrings that render correctly.

Import Order and Side Effects

pdoc calls importlib.import_module() on your package. This means:

  1. All module-level code executes (just like Sphinx autodoc)
  2. Dependencies must be importable
  3. Side effects (network calls, file I/O) will run

For packages with heavy dependencies, use conditional imports:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import numpy as np  # only imported during type checking, not at runtime

Since pdoc uses runtime inspection, TYPE_CHECKING blocks won’t cause import failures. But type annotations referencing those imports will still render correctly because of from __future__ import annotations (PEP 563 stringified annotations).

Custom Templates

pdoc uses Jinja2 templates for HTML rendering. Extract the defaults and customize:

pdoc --template-directory custom_templates/ mypackage

Create custom_templates/module.html.jinja2 to override the default:

{% extends "default/module.html.jinja2" %}

{% block head %}
  {{ super() }}
  <link rel="stylesheet" href="https://your-cdn.com/company-theme.css">
{% endblock %}

{% block nav %}
  <div class="company-header">
    <img src="https://your-cdn.com/logo.svg" alt="Company Logo">
  </div>
  {{ super() }}
{% endblock %}

Available template blocks:

BlockContent
head<head> element (CSS, meta tags)
navNavigation sidebar
module_infoModule-level docstring
membersAll documented members
footerPage footer

Docstring Inheritance

pdoc automatically inherits docstrings from parent classes:

class BaseProcessor:
    def process(self, data: bytes) -> bytes:
        """Transform raw data into processed output.

        Args:
            data: Raw input bytes.

        Returns:
            Processed bytes ready for storage.
        """
        raise NotImplementedError

class JsonProcessor(BaseProcessor):
    def process(self, data: bytes) -> bytes:
        # No docstring here — pdoc uses BaseProcessor.process's docstring
        return json.loads(data)

JsonProcessor.process appears in the docs with the parent’s docstring and a note indicating it’s inherited. Override selectively:

class CsvProcessor(BaseProcessor):
    def process(self, data: bytes) -> bytes:
        """Parse CSV data into normalized output.

        Extends the base processor with CSV-specific delimiter
        detection and header inference.
        """
        ...

Module-Level Documentation

The docstring at the top of a module file becomes the module’s main documentation page. Use this for narrative documentation:

"""
# Database Connection Module

This module manages connection pooling and query execution
for the PostgreSQL backend.

## Architecture

Application → ConnectionPool → PostgreSQL ↓ HealthCheck (periodic)


## Configuration

Set these environment variables:
- `DB_HOST` — database hostname
- `DB_POOL_SIZE` — max concurrent connections (default: 10)

## Usage

```python
from mypackage.db import get_connection

async with get_connection() as conn:
    result = await conn.fetch("SELECT * FROM users")

"""


pdoc renders this Markdown as the first thing visitors see on the module page, before the function/class listings.

## Programmatic API

pdoc can be used as a library for custom documentation tooling:

```python
import pdoc

modules = pdoc.extract.walk_packages(["mypackage"])
docs = {}

for module in modules:
    mod = pdoc.doc.Module.from_name(module)
    docs[module] = {
        "functions": [
            {
                "name": f.name,
                "docstring": f.docstring,
                "parameters": [str(p) for p in f.signature.parameters.values()],
            }
            for f in mod.functions.values()
        ],
        "classes": [c.name for c in mod.classes.values()],
    }

This is useful for building custom documentation indexes, generating API changelogs (diff between versions), or feeding documentation into search engines.

Advanced Rendering Features

Linking Between Objects

pdoc supports several cross-reference syntaxes:

def connect(config: "ConnectionConfig") -> "Connection":
    """Create a connection using the given config.

    See `ConnectionConfig` for available options.
    See `mypackage.pool` for connection pooling.
    For error handling, see `ConnectionError`.
    """

Backtick references are resolved to clickable links. pdoc searches the current module first, then parent packages, then builtins.

Admonitions

pdoc renders Markdown admonitions:

def dangerous_operation():
    """Perform an irreversible operation.

    !!! warning
        This operation cannot be undone. Always create
        a backup before calling this function.

    !!! note
        Requires admin privileges. See `authorize()`.
    """

LaTeX Math

For scientific libraries:

def gaussian(x: float, mu: float, sigma: float) -> float:
    r"""Evaluate the Gaussian probability density function.

    $$f(x) = \\frac{1}{\\sigma\\sqrt{2\\pi}} e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}$$
    """

CI Integration

A minimal GitHub Actions workflow:

name: API Docs
on:
  push:
    branches: [main]

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
      - run: pip install -e . && pip install pdoc
      - run: pdoc mypackage -o docs/api/
      - uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: docs/api/

Documentation Coverage

Check that all public objects have docstrings:

# scripts/check_docstrings.py
import pdoc
import sys

missing = []
for modname in pdoc.extract.walk_packages(["mypackage"]):
    mod = pdoc.doc.Module.from_name(modname)
    for func in mod.functions.values():
        if not func.docstring:
            missing.append(f"{modname}.{func.name}")
    for cls in mod.classes.values():
        if not cls.docstring:
            missing.append(f"{modname}.{cls.name}")

if missing:
    print("Missing docstrings:")
    for name in missing:
        print(f"  - {name}")
    sys.exit(1)

Run this in CI alongside the build to enforce documentation standards.

pdoc vs. Sphinx autodoc vs. mkdocstrings

FeaturepdocSphinx autodocmkdocstrings
Setup timeSeconds30+ minutes10 minutes
Config files needed03+ (conf.py, index.rst, Makefile)1 (mkdocs.yml)
Markdown docstrings✅ native⚠️ via MyST✅ native
Cross-references✅ backticks✅ roles and domains✅ autorefs
Custom themesJinja2 templatesFull Sphinx themesMkDocs themes
Narrative docsModule docstrings onlyFull supportFull support
VersioningManualsphinx-multiversionmike

pdoc’s sweet spot is the project that needs API docs today, not after a documentation sprint. For projects that need a full documentation site with guides, tutorials, and API reference, Sphinx or MkDocs + mkdocstrings is the better investment.

One thing to remember: pdoc proves that good documentation tooling doesn’t need to be complex — by focusing exclusively on API reference and doing it well, it eliminates the setup barrier that stops many projects from having any docs at all.

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.