Auto-Documenting Code with AI: Docstrings, README and CI

A Python script finds functions without docstrings, asks a model to describe them, and inserts the text into the code safely. Plus pre-commit and CI setup.

4 min · · development · автоматизация

Auto-documenting code with AI handles a boring but important job: functions without docstrings get a description of parameters and return values in one consistent style. Below is a script that finds such functions with the ast module, generates text via the API, and inserts it so the file stays valid Python. Then we wire it into pre-commit and GitHub Actions.

Where automation helps and where it does not

It works well for:

  • describing parameters, types and return values from the signature and body;
  • bringing docstrings to one style (Google, NumPy or Sphinx);
  • paying off documentation debt in an old module that has none.

It works worse for what is not in the code: why the function exists, its business constraints, why this particular algorithm was chosen. The author knows that, the model does not. So generated text is a draft that goes through normal code review.

How it works

  1. Parse the file with ast and find public functions and methods without a docstring.
  2. For each, send the function source and the style requirements to the model.
  3. Insert the answer as the first line of the function body with the right indentation.
  4. Check that the file still parses, and only then save it.

The auto_doc.py script

You need Python 3.9+, pip install openai, and a fetch_... key in OPENAI_API_KEY. The script takes a list of files and edits them in place.

bash
#!/usr/bin/env python3
import ast
import os
import sys
import textwrap

from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key=os.environ["OPENAI_API_KEY"],
)
MODEL = os.environ.get("DOC_MODEL", "glm-5.3-flash")

Things to note:

  • Bottom-up insertion. Functions are sorted by line number in reverse, so inserting near the end of the file does not shift line numbers for functions above.
  • Indentation comes from the first statement of the body (col_offset), not a hard-coded "4 spaces", so class methods and nested functions work. Decorators and multi-line signatures do not get in the way either, because the insertion point is computed from the body, not from the def line.
  • ast.parse check before writing. If something goes wrong, the script fails and the original file stays untouched.
  • The model comes from DOC_MODEL. Start with glm-5.3-flash as the cheapest and compare it on 20–30 functions with claude-sonnet-5 or gpt-5.6-sol: keep whichever needs fewer edits in review. The current model list is at GET https://api.zukko.pro/v1/models or on the "Models" page in the dashboard.

Run it with python3 auto_doc.py src/module.py src/utils.py, then git diff and review it like any other change. More about the client is in the Python SDK guide.

A README section for a module

The same approach works for a short module description. To avoid paying for the whole code, send only signatures and existing docstrings:

bash
def module_outline(path: str) -> str:
    with open(path, encoding="utf-8") as f:
        tree = ast.parse(f.read())
    parts = [ast.get_docstring(tree) or ""]
    for node in tree.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            header = ast.unparse(node).splitlines()[0]
            parts.append(f"{header}\n    {ast.get_docstring(node) or ''}")
    return "\n\n".join(parts)


def generate_readme_section(path: str) -> str:
    prompt = (
        "Write a short README section in Markdown for this Python module: "

Running it with pre-commit

Calling an API on every commit slows you down, so it is more convenient to keep the hook in the manual stage, which per the pre-commit documentation is never triggered automatically. .pre-commit-config.yaml:

bash
repos:
  - repo: local
    hooks:
      - id: auto-docstring
        name: Generate missing docstrings
        entry: python scripts/auto_doc.py
        language: python
        additional_dependencies: [openai]
        types: [python]
        stages: [manual]

Run it on demand: pre-commit run auto-docstring --hook-stage manual --all-files.

Running it in GitHub Actions

A pull request workflow that generates docstrings only in changed files and commits the result to the PR branch. The key is stored in the repository secret ZUKKO_API_KEY.

bash
name: auto-docstrings

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

permissions:
  contents: write

jobs:
  docstrings:
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest

Details: ref: github.head_ref lets you push to the PR branch instead of the temporary merge commit; the if condition skips PRs from forks, where secrets are not available; a commit is created only when files changed. Give CI its own key with a daily limit and a list of allowed models; see key limits.

What it costs

Assumptions: the prompt with style requirements is about 100 tokens and the function about 300, so 400 input tokens; the docstring is about 150 output tokens. For 1,000 functions: 400,000 input and 150,000 output.

ModelFormula per 1,000 functionsTotalFor 5,000 functions
glm-5.3-flash ($0.04 / $0.04)(400,000 × 0.04 + 150,000 × 0.04) / 1,000,000$0.022$0.11
claude-sonnet-5 ($0.3 / $0.3)(400,000 × 0.3 + 150,000 × 0.3) / 1,000,000$0.165$0.825

Long functions cost proportionally more: a 2,000-token function uses five times more input. To try it on your project, create a key in the dashboard; setup steps are in the quick start.

FAQ

Can the model describe a function incorrectly?

Yes, especially when behavior depends on external state or non-obvious side effects. That is why generated docstrings go into a normal PR and get reviewed. The prompt says "do not invent parameters or behavior", which lowers the risk but does not remove it.

How do I update docstrings when the code changes?

The script only touches functions without a docstring, so it never overwrites existing text. To update, store a hash of each function body next to the code (for example, in a JSON file) and regenerate the docstring when the hash changes:

bash
import hashlib

def body_hash(node: ast.AST) -> str:
    body = node.body[1:] if ast.get_docstring(node) is not None else node.body
    text = "\n".join(ast.unparse(stmt) for stmt in body)
    return hashlib.sha256(text.encode()).hexdigest()

Can the insertion break my code?

The script checks the result with ast.parse before writing, so it will not save a syntactically broken file. The docstring itself does not affect execution; it is a string stored in the __doc__ attribute. Functions whose body sits on the same line as def are skipped.

Are type hints taken into account?

Yes: the model receives the full function source, annotations included, and can describe parameters with their types. Without annotations, types in the docstring are a guess, so check them in review.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Auto-Generate Code Docs with AI: Docstrings and README · Zukko