Code documentation has two failure modes: missing entirely, or present but wrong. AI tools solve the first problem well and are getting better at the second. This guide covers practical workflows for generating docstrings, API references, and architecture documentation at scale.
Approach 1: Batch Docstring Generation
The highest-ROI use case is adding docstrings to existing functions that have none. This script processes an entire module:
# docstring_generator.py
import ast
import os
from pathlib import Path
from anthropic import Anthropic
client = Anthropic()
def find_undocumented_functions(source: str) -> list[dict]:
"""Find functions and methods that lack docstrings."""
tree = ast.parse(source)
source_lines = source.split("\n")
undocumented = []
for node in ast.walk(tree):
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
# Check if first statement is a docstring
has_docstring = (
node.body and
isinstance(node.body[0], ast.Expr) and
isinstance(node.body[0].value, ast.Constant) and
isinstance(node.body[0].value.value, str)
)
if not has_docstring:
func_source = "\n".join(
source_lines[node.lineno - 1:node.end_lineno]
)
undocumented.append({
"name": node.name,
"lineno": node.lineno,
"source": func_source,
"is_async": isinstance(node, ast.AsyncFunctionDef)
})
return undocumented
def generate_docstring(func_info: dict, style: str = "google") -> str:
"""Generate a docstring for a function using Claude."""
style_example = {
"google": """Args:
param (type): description
Returns:
type: description
Raises:
ErrorType: when condition""",
"numpy": """Parameters
----------
param : type
description
Returns
-------
type
description""",
"sphinx": """:param name: description
:type name: type
:returns: description
:rtype: type"""
}.get(style, "")
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=400,
messages=[{
"role": "user",
"content": f"""Write a {style} style docstring for this Python function.
Rules:
- First line: concise summary (under 72 chars)
- Document all parameters with types (infer from annotations/usage)
- Document return value
- Document exceptions that can be raised
- Note any important side effects
- Do NOT describe the implementation — describe the contract
Return only the docstring content (without triple quotes), no explanation.
Function:
{func_info['source']}"""
}]
)
return response.content[0].text.strip()
def add_docstrings_to_file(
filepath: str,
style: str = "google",
dry_run: bool = True
) -> dict:
"""Add docstrings to all undocumented functions in a file."""
source = Path(filepath).read_text()
undocumented = find_undocumented_functions(source)
if not undocumented:
return {"file": filepath, "added": 0, "functions": []}
additions = []
for func in undocumented:
docstring = generate_docstring(func, style)
additions.append({
"function": func["name"],
"line": func["lineno"],
"docstring": docstring
})
if not dry_run:
# Insert docstrings into source (reverse order to preserve line numbers)
lines = source.split("\n")
for addition in sorted(additions, key=lambda x: x["line"], reverse=True):
# Find the function def line and insert after it
func_line_idx = addition["line"] - 1
# Find indent of function body
body_line = lines[func_line_idx + 1] if func_line_idx + 1 < len(lines) else ""
indent = len(body_line) - len(body_line.lstrip())
indent_str = " " * indent
docstring_lines = [f'{indent_str}"""'] + \
[f'{indent_str}{line}' for line in addition["docstring"].split("\n")] + \
[f'{indent_str}"""', ""]
# Insert after function signature
insert_pos = func_line_idx + 1
lines[insert_pos:insert_pos] = docstring_lines
Path(filepath).write_text("\n".join(lines))
return {"file": filepath, "added": len(additions), "functions": additions}
# Process entire module
def document_module(module_path: str, style: str = "google"):
for py_file in Path(module_path).rglob("*.py"):
if "test_" in py_file.name or "__pycache__" in str(py_file):
continue
result = add_docstrings_to_file(str(py_file), style, dry_run=False)
if result["added"] > 0:
print(f"Added {result['added']} docstrings to {py_file.name}")
Approach 2: Architecture Documentation from Code
def generate_architecture_doc(repo_path: str) -> str:
"""Generate architecture documentation by analyzing the codebase structure."""
# Gather structure
structure = {}
for py_file in Path(repo_path).rglob("*.py"):
if "__pycache__" in str(py_file):
continue
rel_path = py_file.relative_to(repo_path)
try:
source = py_file.read_text()
tree = ast.parse(source)
classes = [n.name for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
functions = [n.name for n in ast.walk(tree)
if isinstance(n, ast.FunctionDef) and n.col_offset == 0]
structure[str(rel_path)] = {
"classes": classes,
"top_level_functions": functions[:5]
}
except Exception:
pass
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=2000,
messages=[{
"role": "user",
"content": f"""Generate architecture documentation for this Python codebase.
File structure with classes and functions:
{json.dumps(structure, indent=2)[:5000]}
Write documentation covering:
1. **Overview**: What does this codebase do? (infer from names)
2. **Module Structure**: Purpose of each module/package
3. **Key Components**: Most important classes and their responsibilities
4. **Data Flow**: How data moves through the system (infer from module names)
5. **External Dependencies**: Any obvious external integrations
Format as markdown. Be concise — one paragraph per major component."""
}]
)
return response.content[0].text
# Save to docs/
Path("docs/architecture.md").write_text(
generate_architecture_doc("src/")
)
Approach 3: API Reference Generation with MkDocs
# generate_api_docs.py
def generate_mkdocs_page(module_path: str) -> str:
"""Generate a MkDocs-compatible markdown page for a Python module."""
source = Path(module_path).read_text()
module_name = Path(module_path).stem
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=3000,
messages=[{
"role": "user",
"content": f"""Generate MkDocs-compatible API documentation for this Python module.
Module: {module_name}
Format:
- Module-level description
- For each public class:
- Class description
- Constructor parameters table
- Methods with parameters, return types, and brief descriptions
- Code example
- For each public function:
- Function signature
- Description
- Parameters table (Name | Type | Required | Description)
- Returns section
- Example usage
Use code blocks for examples. Mark internal methods (starting with _) as private.
Source:
{source[:6000]}"""
}]
)
return response.content[0].text
# GitHub Action to regenerate on push
# .github/workflows/docs.yml
name: Update API Docs
on:
push:
branches: [main]
paths:
- 'src/**/*.py'
jobs:
generate-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate API docs
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
pip install anthropic mkdocs mkdocs-material
python scripts/generate_api_docs.py
mkdocs build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
Approach 4: Inline Comment Quality Review
AI can also review existing documentation for accuracy:
def review_documentation_accuracy(source_code: str) -> list[dict]:
"""Identify documentation that may be inaccurate or outdated."""
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1500,
messages=[{
"role": "user",
"content": f"""Review the documentation in this code for accuracy.
Look for:
1. Docstrings that don't match the function signature (wrong params, types)
2. Comments that describe what the code does but are no longer accurate
3. TODO comments that reference tickets or issues
4. Docstrings promising behavior the code doesn't implement
For each issue found:
FILE_LINE: approximate line number
ISSUE_TYPE: [wrong_params / outdated_behavior / missing_docs / todo_comment]
DESCRIPTION: What's wrong
SUGGESTION: How to fix
Source:
{source_code[:6000]}"""
}]
)
return response.content[0].text
# Run on every PR as a documentation quality gate
Approach 5: Changelog and Release Notes Generation
One underused application is generating changelogs directly from git commits and diffs. This eliminates the friction of writing release notes manually:
import subprocess
def generate_changelog_entry(version: str, since_tag: str) -> str:
"""Generate a changelog entry from git commits since the last tag."""
# Get commit log since last tag
log = subprocess.check_output([
"git", "log", f"{since_tag}..HEAD",
"--pretty=format:%h %s",
"--no-merges"
]).decode()
# Get the full diff for context
diff_stat = subprocess.check_output([
"git", "diff", f"{since_tag}..HEAD", "--stat"
]).decode()
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1000,
messages=[{
"role": "user",
"content": f"""Generate a changelog entry for version {version}.
Git commits since {since_tag}:
{log[:3000]}
Files changed:
{diff_stat[:1000]}
Format as Keep a Changelog (https://keepachangelog.com):
## [{version}] - YYYY-MM-DD
### Added
### Changed
### Fixed
### Removed
Group commits by type. Skip trivial commits (typos, formatting).
Translate technical commit messages into user-facing language."""
}]
)
return response.content[0].text
This pairs well with a pre-release GitHub Action that auto-creates a draft release with the generated changelog, which an engineer then reviews and edits before publishing.
Tool Comparison
| Tool | Docstrings | API Ref | Architecture Docs | Accuracy Review | Changelog |
|---|---|---|---|---|---|
| Claude (API) | Excellent, infers types | Full markdown | Yes | Yes | Yes |
| GPT-4 (API) | Good | Good | Basic | Partial | Good |
| Sphinx autodoc | From existing docstrings | HTML output | No | No | No |
| pydoc-markdown | From existing docstrings | Markdown | No | No | No |
| Copilot (inline) | In-editor only | No batch | No | No | No |
The AI-based approaches (Claude and GPT-4) are the only ones that generate documentation where none exists. Tools like Sphinx require docstrings to already be present.
Claude vs GPT-4 for documentation: Claude follows style guides more consistently. When you tell it to use Google style docstrings throughout a 20-function module, it stays consistent where GPT-4 may drift between styles. For changelog generation, both tools perform similarly, but Claude produces more user-focused language rather than mirroring the raw commit messages.
Integration with Pre-commit Hooks
To enforce documentation as part of the development workflow, add a pre-commit hook that blocks commits with undocumented public functions:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: check-docstrings
name: Check for missing docstrings
entry: python scripts/check_docstrings.py
language: python
types: [python]
pass_filenames: true
# scripts/check_docstrings.py
import ast
import sys
def check_file(filepath: str) -> list[str]:
source = open(filepath).read()
tree = ast.parse(source)
missing = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if node.name.startswith("_"):
continue # Skip private methods
has_doc = (node.body and isinstance(node.body[0], ast.Expr)
and isinstance(node.body[0].value, ast.Constant))
if not has_doc:
missing.append(f"{filepath}:{node.lineno}: {node.name}() missing docstring")
return missing
issues = []
for f in sys.argv[1:]:
issues.extend(check_file(f))
if issues:
print("\n".join(issues))
sys.exit(1)
This keeps docstring debt from accumulating without requiring every developer to remember to write them. Pair it with a weekly CI job that uses Claude to auto-generate drafts for any remaining gaps.
Related Articles
- AI Tools for Automated Code Documentation Generation in 2026
- Best AI Tools for Code Documentation Generation 2026
- Best AI Tools for Generating API Documentation From Code
- AI Tools for Automated API Documentation from Code Comments
- Best AI Tools for Automated Code Review 2026 Built by theluckystrike — More at zovo.one