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:
- All module-level code executes (just like Sphinx autodoc)
- Dependencies must be importable
- 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:
| Block | Content |
|---|---|
head | <head> element (CSS, meta tags) |
nav | Navigation sidebar |
module_info | Module-level docstring |
members | All documented members |
footer | Page 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
| Feature | pdoc | Sphinx autodoc | mkdocstrings |
|---|---|---|---|
| Setup time | Seconds | 30+ minutes | 10 minutes |
| Config files needed | 0 | 3+ (conf.py, index.rst, Makefile) | 1 (mkdocs.yml) |
| Markdown docstrings | ✅ native | ⚠️ via MyST | ✅ native |
| Cross-references | ✅ backticks | ✅ roles and domains | ✅ autorefs |
| Custom themes | Jinja2 templates | Full Sphinx themes | MkDocs themes |
| Narrative docs | Module docstrings only | Full support | Full support |
| Versioning | Manual | sphinx-multiversion | mike |
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.
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.