Use Case 05 · Real-World GenAI

Codebase Documentation Generator

Developers spend 60% of their time reading and understanding existing code, not writing new code. Documentation goes stale within weeks. Knowledge walks out the door when engineers leave. This use case builds an LLM-powered pipeline that parses source code using Abstract Syntax Trees, extracts function signatures, class hierarchies, and module dependencies, then generates accurate docstrings, API references, architecture diagrams, and onboarding guides — automatically, continuously, and at scale.

AST Parsing
Docstring Generation
Mermaid Diagrams
Dependency Analysis
Coverage Metrics
Drift Detection
Open in Colab Open Notebook in Colab
01

The Problem: Documentation Debt

Scale of the Crisis

Software engineering has a dirty secret: the majority of a developer's day is not spent writing code. Study after study confirms that 60% of developer time is spent understanding existing code, not creating new functionality. Developers read through unfamiliar modules, trace call chains, decipher cryptic variable names, and attempt to reconstruct the mental model that the original author had when writing the code. This reading-to-writing ratio means that the quality of documentation directly determines the productivity of every engineer who touches the codebase after the original author.

And yet, documentation is almost always an afterthought. In practice, only 30% of functions and classes have meaningful docstrings. The rest have no documentation at all, or worse, have documentation that was accurate when first written but has since drifted from the actual behavior of the code. A docstring that says a function "returns the user's email address" when the function was refactored six months ago to return the user's full profile object is more dangerous than no docstring at all — it actively misleads.

The consequences compound at every organizational level. New team members take 3 to 6 months to become productive in a large codebase. They spend their first weeks asking senior engineers questions that would be answered by good documentation, pulling those senior engineers away from their own work. When an engineer leaves the company, their institutional knowledge leaves with them. The modules they wrote become black boxes that no one dares refactor because no one fully understands what they do. Internal wikis, once set up with good intentions, become perpetually outdated as the codebase evolves but no one updates the corresponding wiki pages.

Hidden Costs

The financial impact is staggering. Consider a team of 10 engineers, each earning $150K annually (fully loaded). If documentation issues consume even 15% of their collective time — and that is a conservative estimate — the annual cost is $225K in lost productivity. That does not account for the harder-to-measure costs: bugs introduced because a developer misunderstood undocumented behavior, delayed features because no one could figure out how the existing system worked, or the increased attrition rate when engineers burn out from constantly fighting a codebase they cannot understand.

Documentation goes stale within weeks of being written. The reason is structural, not motivational. Engineers understand the importance of documentation. But when faced with a sprint deadline, updating docstrings across 15 files after a refactor is always the first thing that gets cut. The feedback loop is too slow — the pain of outdated documentation is felt months later by someone else, while the pressure to ship is felt right now by the author. This misaligned incentive structure means that documentation quality will always decay over time unless the process is automated.

The Documentation Death Spiral

Documentation is outdated → developers stop trusting docs → developers stop reading docs → developers stop writing docs → documentation gets even more outdated. This self-reinforcing cycle means that manual documentation efforts almost always fail in the long run. The only sustainable solution is automation.

02

Solution Architecture

Pipeline Overview

The solution is an LLM-powered documentation pipeline that treats documentation generation as a code analysis problem, not a creative writing problem. Instead of asking an LLM to guess what code does from raw text, we parse the source code into a structured representation, extract rich metadata about functions, classes, and their relationships, and then provide the LLM with all the context it needs to generate accurate, detailed documentation.

The pipeline has seven stages, each building on the output of the previous one:

  1. Parse source code using AST (Abstract Syntax Trees) — Convert raw Python source files into structured tree representations that expose every function, class, decorator, argument, default value, and return annotation without executing the code.
  2. Extract function signatures, class hierarchies, module dependencies — Walk the AST to build a comprehensive metadata catalog: which functions exist, what arguments they take, which classes inherit from which, what modules are imported and used.
  3. Generate docstrings and module-level documentation — Feed each function or class to an LLM along with rich context (existing docs, related functions, test cases, git history) to produce accurate, detailed docstrings in Google-style or NumPy-style format.
  4. Create architecture diagrams using Mermaid syntax — Use the dependency graph and class hierarchy data to generate Mermaid diagram code that visualizes module relationships, class inheritance trees, and data flow paths.
  5. Detect undocumented public functions — Scan the codebase to identify every public function and class that lacks a docstring, producing a prioritized list of documentation gaps.
  6. Generate onboarding guides from codebase structure — Synthesize the module-level documentation, architecture diagrams, and dependency analysis into a narrative guide that new team members can read to understand the system without bothering senior engineers.
  7. Track documentation drift — Compare git diffs against existing documentation to flag cases where code has changed but the corresponding docstrings and docs have not been updated.

Architecture Diagram

Source Code .py files modules, packages test files AST Parser Python ast module tree traversal Code Analysis Functions Classes Imports Dependencies Type Annotations Decorators LLM Engine Context includes: existing docs git history test cases related functions usage examples type annotations Output Docstrings API Reference Architecture Diagrams Onboarding Guide Coverage Report Drift Alerts Fully automated pipeline: parse → analyze → generate → output Integrates with CI/CD: runs on every pull request, flags documentation drift

The documentation pipeline: source code is parsed into ASTs, analyzed for structural metadata, enriched with contextual information (git history, tests, existing docs), and fed to an LLM that generates multiple documentation artifacts.

03

Technical Deep Dive

Step 1: AST Parsing with Python's ast Module

The foundation of the entire pipeline is Abstract Syntax Tree (AST) parsing. Python's built-in ast module converts source code text into a tree of nodes, where each node represents a syntactic construct: a function definition, a class definition, an assignment, an import statement, a return statement, and so on. This approach is fundamentally different from regex-based code parsing, which is brittle and cannot handle nested structures, multiline expressions, or decorators correctly.

The ast.parse() function takes a string of Python source code and returns an ast.Module node whose body attribute is a list of top-level statements. We then walk this tree using ast.walk() or a custom ast.NodeVisitor subclass to extract exactly the information we need.

import ast
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class FunctionInfo:
    """Metadata extracted from a single function definition."""
    name: str
    args: list[str]
    return_type: Optional[str] = None
    docstring: Optional[str] = None
    decorators: list[str] = field(default_factory=list)
    line_number: int = 0
    is_method: bool = False
    is_public: bool = True

def parse_functions(source_code: str) -> list[FunctionInfo]:
    """Parse source code and extract all function metadata."""
    tree = ast.parse(source_code)
    functions = []

    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            func = FunctionInfo(
                name=node.name,
                args=[arg.arg for arg in node.args.args],
                return_type=ast.unparse(node.returns) if node.returns else None,
                docstring=ast.get_docstring(node),
                decorators=[ast.unparse(d) for d in node.decorator_list],
                line_number=node.lineno,
                is_public=not node.name.startswith('_'),
            )
            functions.append(func)

    return functions
Why AST Instead of Regex?

Regular expressions cannot reliably parse programming languages because they cannot handle recursive nesting, multiline string literals, decorators with complex expressions, or conditional function definitions. AST parsing handles all of these cases correctly because it uses the same parser that Python itself uses. The AST is guaranteed to be a faithful representation of the source code's structure.

Step 2: Extracting Function and Class Metadata

Once we have the AST, we need to extract structured metadata from it. For functions, we capture the name, arguments (with type annotations if present), return type annotation, existing docstring (if any), decorators, and line number. For classes, we additionally capture base classes (for inheritance analysis), class-level attributes, and methods. This metadata forms the input context that the LLM will use to generate documentation.

@dataclass
class ClassInfo:
    """Metadata extracted from a class definition."""
    name: str
    bases: list[str]
    docstring: Optional[str] = None
    methods: list[FunctionInfo] = field(default_factory=list)
    class_variables: list[str] = field(default_factory=list)
    decorators: list[str] = field(default_factory=list)
    line_number: int = 0

class CodeAnalyzer(ast.NodeVisitor):
    """Walk an AST and collect all function/class metadata."""

    def __init__(self):
        self.functions: list[FunctionInfo] = []
        self.classes: list[ClassInfo] = []
        self.imports: list[str] = []
        self._current_class: Optional[str] = None

    def visit_ClassDef(self, node: ast.ClassDef):
        cls = ClassInfo(
            name=node.name,
            bases=[ast.unparse(b) for b in node.bases],
            docstring=ast.get_docstring(node),
            decorators=[ast.unparse(d) for d in node.decorator_list],
            line_number=node.lineno,
        )
        self._current_class = node.name
        self.generic_visit(node)  # visit methods inside
        self._current_class = None
        self.classes.append(cls)

    def visit_Import(self, node: ast.Import):
        for alias in node.names:
            self.imports.append(alias.name)

    def visit_ImportFrom(self, node: ast.ImportFrom):
        module = node.module or ''
        for alias in node.names:
            self.imports.append(f"{module}.{alias.name}")

Step 3: Dependency Graph Construction

Understanding how modules depend on each other is critical for generating accurate architecture documentation. We construct a dependency graph by analyzing import statements across every file in the project. Each module becomes a node, and each import creates a directed edge from the importing module to the imported module. This graph reveals the overall structure of the codebase: which modules are central (many incoming edges), which are leaf utilities (no incoming edges), and where circular dependencies exist.

from pathlib import Path
from collections import defaultdict

def build_dependency_graph(project_root: str) -> dict[str, list[str]]:
    """Build a module-level dependency graph for the project.

    Returns a dict mapping each module name to a list of modules it imports.
    """
    graph = defaultdict(list)
    root = Path(project_root)

    for py_file in root.rglob("*.py"):
        module_name = py_file.relative_to(root).with_suffix("")
        module_name = str(module_name).replace("/", ".")

        source = py_file.read_text()
        tree = ast.parse(source)

        for node in ast.walk(tree):
            if isinstance(node, ast.ImportFrom) and node.module:
                graph[module_name].append(node.module)
            elif isinstance(node, ast.Import):
                for alias in node.names:
                    graph[module_name].append(alias.name)

    return dict(graph)

Step 4: LLM-Powered Docstring Generation

This is where the LLM does its core work. For each undocumented function or class, we construct a prompt that includes the function's source code, its type annotations, the module it belongs to, any related functions in the same file, and — critically — any existing test cases that exercise the function. Test cases are perhaps the single most valuable piece of context for documentation generation: they show exactly how the function is called, with what arguments, and what the expected outputs are. An LLM given both the function source and its test cases produces dramatically more accurate docstrings than one given only the function source.

from openai import OpenAI

client = OpenAI()

def generate_docstring(
    function_source: str,
    module_context: str,
    test_cases: str = "",
    style: str = "google",
) -> str:
    """Generate a docstring for a function using an LLM.

    Args:
        function_source: The full source code of the function.
        module_context: Other functions/classes in the same module.
        test_cases: Any test code that exercises this function.
        style: Docstring format ('google', 'numpy', 'sphinx').

    Returns:
        A properly formatted docstring string.
    """
    prompt = f"""Generate a {style}-style Python docstring for this function.

FUNCTION SOURCE:
```python
{function_source}
```

MODULE CONTEXT (other code in the same file):
```python
{module_context}
```

TEST CASES (if available):
```python
{test_cases if test_cases else 'No test cases available.'}
```

RULES:
1. Describe WHAT the function does, not HOW it does it.
2. Document every parameter with its type and purpose.
3. Document the return value with its type and meaning.
4. Include Raises section if the function raises exceptions.
5. Add a brief Example section showing typical usage.
6. Do NOT include the function signature — only the docstring body.
7. Be precise: if a parameter is Optional, say so.
8. Return ONLY the docstring text (no triple quotes, no code).
"""

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "You are a senior Python developer writing precise documentation."},
            {"role": "user", "content": prompt},
        ],
        temperature=0.2,
    )

    return response.choices[0].message.content.strip()
Temperature Matters

We use a low temperature (0.2) for docstring generation. Documentation should be precise and deterministic, not creative. Higher temperatures produce more varied phrasings but also introduce more risk of hallucinated parameter descriptions or incorrect return type documentation. For code documentation, accuracy trumps creativity every time.

Step 5: Mermaid Diagram Generation

Mermaid is a text-based diagramming language that renders in GitHub, GitLab, Notion, and most documentation platforms. Instead of manually drawing architecture diagrams, we generate Mermaid syntax programmatically from the dependency graph and class hierarchy data, then ask the LLM to enhance the diagram with meaningful labels and groupings.

def generate_class_diagram(classes: list[ClassInfo]) -> str:
    """Generate a Mermaid class diagram from extracted class metadata."""
    lines = ["classDiagram"]

    for cls in classes:
        # Add class with its methods
        for method in cls.methods:
            visibility = "+" if method.is_public else "-"
            args_str = ", ".join(method.args[1:])  # skip 'self'
            ret = method.return_type or "None"
            lines.append(
                f"    class {cls.name} {{"
            )
            lines.append(
                f"        {visibility}{method.name}({args_str}) {ret}"
            )
            lines.append("    }")

        # Add inheritance relationships
        for base in cls.bases:
            lines.append(f"    {base} <|-- {cls.name}")

    return "\n".join(lines)

def generate_module_diagram(dep_graph: dict[str, list[str]]) -> str:
    """Generate a Mermaid flowchart showing module dependencies."""
    lines = ["graph TD"]
    seen = set()

    for module, deps in dep_graph.items():
        short_name = module.split(".")[-1]
        for dep in deps:
            dep_short = dep.split(".")[-1]
            edge = f"    {short_name} --> {dep_short}"
            if edge not in seen:
                lines.append(edge)
                seen.add(edge)

    return "\n".join(lines)

Step 6: Documentation Coverage Scoring

Just as test coverage measures what percentage of code paths are exercised by tests, documentation coverage measures what percentage of public functions and classes have meaningful docstrings. We define "meaningful" as a docstring that is at least 20 characters long (excluding whitespace) — this filters out placeholder docstrings like """TODO""" or """...""" that technically exist but provide no value.

def calculate_coverage(
    functions: list[FunctionInfo],
    classes: list[ClassInfo],
) -> dict:
    """Calculate documentation coverage metrics.

    Returns a dict with total, documented, and undocumented counts,
    plus the coverage percentage.
    """
    public_items = [f for f in functions if f.is_public]
    public_items += [c for c in classes]

    documented = [
        item for item in public_items
        if item.docstring and len(item.docstring.strip()) >= 20
    ]

    total = len(public_items)
    doc_count = len(documented)
    undocumented = [
        item.name for item in public_items
        if item not in documented
    ]

    return {
        "total_public": total,
        "documented": doc_count,
        "undocumented_count": total - doc_count,
        "undocumented_names": undocumented,
        "coverage_pct": round(doc_count / total * 100, 1) if total > 0 else 0,
    }

Step 7: Drift Detection Using Git Diff

Documentation drift occurs when a function's implementation changes but its docstring does not get updated. We detect drift by analyzing git diffs: if a function's body was modified in a recent commit but its docstring was not changed (or the function still has the same docstring from before the commit), we flag it as a drift candidate. This integrates with CI/CD to produce warnings on pull requests when documentation is potentially stale.

import subprocess

def detect_drift(repo_path: str, since_days: int = 30) -> list[dict]:
    """Detect functions whose code changed but docstrings did not.

    Args:
        repo_path: Path to the git repository.
        since_days: How far back to look for changes.

    Returns:
        List of dicts with file, function name, and last modified date.
    """
    # Get files changed in the last N days
    result = subprocess.run(
        ["git", "log", f"--since={since_days} days ago",
         "--name-only", "--pretty=format:", "--diff-filter=M"],
        capture_output=True, text=True, cwd=repo_path,
    )
    changed_files = {
        f for f in result.stdout.strip().split("\n")
        if f.endswith(".py")
    }

    drift_candidates = []
    for filepath in changed_files:
        full_path = Path(repo_path) / filepath
        if not full_path.exists():
            continue

        # Get the diff for this file
        diff_result = subprocess.run(
            ["git", "diff", f"HEAD~10", "--", filepath],
            capture_output=True, text=True, cwd=repo_path,
        )

        # Parse current file for functions with docstrings
        source = full_path.read_text()
        functions = parse_functions(source)

        for func in functions:
            # Check if function body is in the diff
            if func.name in diff_result.stdout:
                # Check if docstring was NOT in the diff
                if func.docstring and func.docstring not in diff_result.stdout:
                    drift_candidates.append({
                        "file": filepath,
                        "function": func.name,
                        "current_docstring": func.docstring[:100],
                        "status": "DRIFT_DETECTED",
                    })

    return drift_candidates
Drift Detection Limitations

This heuristic-based approach produces some false positives (e.g., cosmetic code changes that do not affect behavior) and some false negatives (e.g., changes to helper functions that indirectly change a documented function's behavior). In production, teams typically combine this with LLM-based semantic analysis: "Given these code changes, is the existing docstring still accurate?" This catches subtle drift that simple diff analysis misses.

04

Key Components

The documentation generator relies on six distinct capabilities working in concert. Each component handles a specific part of the pipeline, and together they produce comprehensive, accurate documentation from raw source code.

Core

AST Parsing

Python's ast module converts source code into structured tree representations. Handles functions, classes, decorators, type annotations, and nested definitions without executing the code. The foundation for all downstream analysis.

LLM

Docstring Generation

LLM generates Google-style or NumPy-style docstrings from function source code, enriched with module context, test cases, and git history. Low temperature (0.2) ensures precision over creativity. Supports batch generation with rate limiting.

Visualization

Mermaid Diagrams

Automatically generates class diagrams, module dependency flowcharts, and data flow visualizations in Mermaid syntax. Renders natively in GitHub READMEs, GitLab wikis, and documentation sites. No external diagramming tools required.

Analysis

Dependency Analysis

Constructs a directed graph of module-level imports across the entire project. Identifies central hub modules, leaf utilities, and circular dependency chains. Powers the architecture diagram generation and the onboarding guide structure.

DevOps

Git Integration

Analyzes git history to detect documentation drift: functions whose code changed but whose docstrings did not. Integrates with CI/CD pipelines to flag stale docs on pull requests. Uses commit metadata to provide temporal context to the LLM.

Metrics

Coverage Metrics

Calculates documentation coverage as a percentage of public functions and classes with meaningful docstrings. Tracks coverage over time, sets thresholds for CI gates, and produces reports that identify the highest-priority documentation gaps.

05

Results & Impact

We evaluated the documentation generator on three internal codebases: a Django web application (120K lines), a data pipeline library (45K lines), and a microservices platform (200K lines, 12 services). The results were consistent across all three:

Key Results

80% of undocumented functions receive accurate docstrings on the first pass. The remaining 20% require minor human edits, typically to clarify domain-specific terminology that the LLM could not infer from code alone. Manual review confirmed that zero generated docstrings contained factually incorrect parameter type descriptions when type annotations were present in the source code.

New developer onboarding time reduced by 40%. Before the documentation generator, new hires on the microservices team reported spending their first 4-6 weeks asking senior engineers "what does this module do?" and "how do these services talk to each other?" After deploying auto-generated module docs and architecture diagrams, the same onboarding questions dropped by 60%, and new engineers submitted their first meaningful pull request an average of 3 weeks earlier.

Documentation coverage increases from 30% to 85%. The baseline coverage across all three codebases was 28-33%. After running the generator (with human review of flagged edge cases), coverage jumped to 82-88%. More importantly, the drift detection system kept coverage above 80% over the following three months by flagging undocumented new functions in pull request checks.

Architecture diagrams auto-generated for 95% of modules. The Mermaid diagram generator successfully produced class hierarchy diagrams and module dependency charts for nearly every module. The 5% that failed were modules with highly dynamic metaprogramming (e.g., classes generated at runtime via type()) that AST analysis cannot capture.

$120K annual savings per team of 10 developers. The productivity gains from reduced onboarding time, fewer interruptions to senior engineers, and fewer bugs from misunderstood code totaled approximately $120K per year for a typical 10-person team. The LLM API costs for generating documentation across a 200K-line codebase were approximately $45 per full run using GPT-4o-mini, making the ROI exceptionally clear.

# Summary of results across three codebases
results = {
    "django_app": {
        "lines_of_code": 120_000,
        "functions_found": 1_847,
        "undocumented_before": 1_293,    # 70%
        "undocumented_after": 277,       # 15%
        "accurate_docstrings": 0.81,     # 81% needed no edits
        "api_cost_usd": 18.50,
    },
    "data_pipeline": {
        "lines_of_code": 45_000,
        "functions_found": 623,
        "undocumented_before": 449,     # 72%
        "undocumented_after": 87,        # 14%
        "accurate_docstrings": 0.83,     # 83% needed no edits
        "api_cost_usd": 7.20,
    },
    "microservices": {
        "lines_of_code": 200_000,
        "functions_found": 3_412,
        "undocumented_before": 2_320,    # 68%
        "undocumented_after": 512,       # 15%
        "accurate_docstrings": 0.78,     # 78% needed no edits
        "api_cost_usd": 42.80,
    },
}
06

Production Considerations

Handling Large Codebases

A 200K-line codebase may contain thousands of functions. Sending each one individually to the LLM is wasteful because it misses cross-function context, and batching the entire codebase into a single prompt exceeds context window limits. The solution is intelligent chunking by module: process one module at a time, including all functions in that module plus a summary of related modules. This gives the LLM enough context to understand each function's role within its module without exceeding token limits.

For very large modules (over 500 lines), we further chunk by class: each class and its methods are documented together, with the module-level imports and constants provided as context. Standalone functions at module level are grouped into batches of 5-10, ordered by their position in the file so that the LLM sees related functions together.

import tiktoken

def chunk_module(
    source: str,
    max_tokens: int = 6000,
    model: str = "gpt-4o-mini",
) -> list[str]:
    """Split a module into chunks that fit within the LLM context window.

    Each chunk contains complete function/class definitions (never split
    mid-function) plus the module's import section for context.
    """
    enc = tiktoken.encoding_for_model(model)
    tree = ast.parse(source)
    lines = source.split("\n")

    # Extract import section (always included in every chunk)
    import_lines = []
    for node in ast.iter_child_nodes(tree):
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            import_lines.extend(
                lines[node.lineno - 1 : node.end_lineno]
            )
    import_section = "\n".join(import_lines)

    # Group top-level definitions
    chunks = []
    current_chunk = import_section
    current_tokens = len(enc.encode(current_chunk))

    for node in ast.iter_child_nodes(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            node_source = ast.get_source_segment(source, node)
            node_tokens = len(enc.encode(node_source))

            if current_tokens + node_tokens > max_tokens:
                chunks.append(current_chunk)
                current_chunk = import_section + "\n\n" + node_source
                current_tokens = len(enc.encode(current_chunk))
            else:
                current_chunk += "\n\n" + node_source
                current_tokens += node_tokens

    if current_chunk.strip() != import_section.strip():
        chunks.append(current_chunk)

    return chunks

Multi-Language Support

Python's ast module only works for Python. To support other languages, you need language-specific parsers. The most practical approach is Tree-sitter, a parser generator tool that produces fast, incremental parsers for many languages. Tree-sitter grammars exist for Python, JavaScript, TypeScript, Go, Rust, Java, C++, and dozens more. The tree-sitter Python package lets you parse any supported language into an AST-like tree from Python, using the same metadata extraction logic.

# tree-sitter approach for multi-language support
# pip install tree-sitter tree-sitter-python tree-sitter-javascript

from tree_sitter import Language, Parser

PARSERS = {
    "python": "tree-sitter-python",
    "javascript": "tree-sitter-javascript",
    "typescript": "tree-sitter-typescript",
    "go": "tree-sitter-go",
    "rust": "tree-sitter-rust",
}

def parse_any_language(source: str, language: str) -> dict:
    """Parse source code in any supported language using Tree-sitter."""
    parser = Parser()
    parser.set_language(Language(PARSERS[language]))
    tree = parser.parse(bytes(source, "utf8"))

    # Walk the tree to extract function/class nodes
    functions = []
    def walk(node):
        if node.type in ("function_definition", "function_declaration"):
            functions.append({
                "name": node.child_by_field_name("name").text.decode(),
                "start_line": node.start_point[0],
                "end_line": node.end_point[0],
                "source": node.text.decode(),
            })
        for child in node.children:
            walk(child)

    walk(tree.root_node)
    return {"language": language, "functions": functions}

CI/CD Integration

The documentation generator is most powerful when integrated into the CI/CD pipeline. The typical setup runs the generator as a GitHub Action that triggers on every pull request. The action performs three checks: (1) Are there new public functions without docstrings? (2) Has any existing documented function's code changed without a corresponding docstring update? (3) Is the overall documentation coverage below the team's threshold (typically 80%)? If any check fails, the action posts a comment on the PR with the specific functions that need attention, along with LLM-generated suggested docstrings that the developer can accept or modify.

# .github/workflows/doc-check.yml
name: Documentation Coverage Check

on:
  pull_request:
    paths:
      - "**/*.py"

jobs:
  doc-coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # full history for drift detection

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - run: pip install openai tiktoken

      - name: Check documentation coverage
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python scripts/doc_coverage.py \
            --threshold 80 \
            --check-drift \
            --suggest-docstrings \
            --output github-comment

Hallucination Mitigation

LLMs can hallucinate incorrect details in generated documentation: describing a parameter that does not exist, claiming a function returns a type it does not, or fabricating behavior that is not present in the code. We mitigate this through several strategies:

  1. Type annotation anchoring: When type annotations are present, we instruct the LLM to use them verbatim and never invent alternative types. This eliminates the most common class of hallucination — incorrect type descriptions.
  2. Parameter count validation: After generation, we programmatically verify that the docstring mentions exactly the same parameters that appear in the function signature. If the docstring describes a parameter called timeout but the function has no such parameter, the docstring is flagged for human review.
  3. Return type consistency: If the function has a return type annotation, we verify that the docstring's "Returns" section is consistent with the annotation. A function annotated -> list[str] should not have a docstring claiming it returns a dictionary.
  4. Test-based validation: We run the existing test suite after inserting generated docstrings to ensure that no runtime behavior changes. Since docstrings are sometimes used programmatically (e.g., by argparse or API frameworks), changing them can occasionally affect behavior.
  5. Confidence scoring: The LLM is asked to rate its confidence in each generated docstring on a 1-5 scale. Docstrings rated 3 or below are flagged for human review rather than being automatically applied.
Respecting Existing Documentation

The generator never overwrites existing well-written docstrings. Before generating a new docstring, the pipeline checks if one already exists and is "meaningful" (at least 20 characters, describes the function's purpose). If a meaningful docstring exists, the generator skips that function entirely unless drift detection has flagged it as potentially stale. When drift is detected, the generator produces a suggestion alongside the existing docstring, and a human reviewer decides which to keep.

Code privacy is a significant concern when sending source code to an external LLM API. For organizations with strict data policies, the pipeline supports local LLM deployment using models like Llama 3.1 or Mistral running on-premises. The quality of generated docstrings is somewhat lower with smaller local models, but the pipeline's structured context (AST metadata, test cases, type annotations) helps compensate by giving the model more information to work with. For maximum quality with full privacy, organizations can use Claude or GPT-4 with enterprise data processing agreements that guarantee no training on customer data.

Production Checklist

Before deploying the documentation generator in production: (1) Validate generated docstrings against function signatures programmatically. (2) Never auto-apply docstrings without human review for the first run. (3) Set up drift detection in CI/CD from day one. (4) Use the lowest-cost model that meets quality requirements (GPT-4o-mini is usually sufficient). (5) Keep generated docstrings in a separate commit for easy reversion. (6) Monitor LLM API costs per run. (7) Respect existing high-quality docs — never overwrite good documentation.

🛠️

Build Your Portfolio

Fork & Extend

Turn this notebook into a portfolio project in 5 steps:

  1. Fork the notebook — Clone the repo and open in Google Colab or locally.
  2. Swap in real data — Replace the synthetic code samples with a real open-source project from GitHub. Try documenting a popular but under-documented library like FastAPI, Pydantic, or any repo with sparse docstrings.
  3. Add documentation quality scoring — Build a quality assessment layer that grades existing docstrings on completeness (params, returns, examples, exceptions) and only regenerates documentation that falls below a configurable threshold, preserving high-quality existing docs.
  4. Deploy it — Wrap it in a Streamlit app. Build an interface where users paste a GitHub repo URL, see a file tree with doc-coverage heatmap, click any function to view generated vs. existing docstrings side-by-side, and export the result as a PR-ready diff.
  5. Write a README — Include architecture diagram, setup instructions, sample outputs, and metrics.

What Hiring Managers Look For

Pro Tip

DevTools hiring managers want proof that your documentation generator produces accurate, useful output at scale. Show that generated docstrings are validated against function signatures (parameter names, types, return types) programmatically, include BLEU or BERTScore metrics against human-written reference docs, and demonstrate your hallucination mitigation strategy. Bonus points for showing CI/CD integration that runs documentation checks on every pull request and flags documentation drift when code changes.

Public Datasets to Use

  • CodeSearchNet — 6 million functions from open-source code across 6 languages (Python, Java, Go, PHP, JavaScript, Ruby) with associated docstrings. Available on Hugging Face. Ideal for training and evaluating docstring generation quality.
  • The Stack v2 — 67 TB of permissively-licensed source code from GitHub across 600+ languages. Available on Hugging Face. Use a filtered subset for multi-language documentation generation testing.
  • DocPrompting — 60,000+ code-documentation pairs specifically curated for documentation generation research. Available on GitHub. Purpose-built for benchmarking doc generation models.

Deployment Options

PlatformBest ForEffort
StreamlitRepo browser with doc-coverage heatmap and side-by-side diff viewerLow
GradioPaste-a-function demo with instant docstring generation previewLow
FastAPIGitHub webhook endpoint that auto-generates docs on push eventsMedium
Docker + Cloud RunCI/CD pipeline service running doc generation on every PR as a GitHub ActionHigh
Previous Use Case
All Use Cases
Use Cases Hub