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
- Parse the file with
astand find public functions and methods without a docstring. - For each, send the function source and the style requirements to the model.
- Insert the answer as the first line of the function body with the right indentation.
- 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.
#!/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 thedefline. ast.parsecheck before writing. If something goes wrong, the script fails and the original file stays untouched.- The model comes from
DOC_MODEL. Start withglm-5.3-flashas the cheapest and compare it on 20–30 functions withclaude-sonnet-5orgpt-5.6-sol: keep whichever needs fewer edits in review. The current model list is atGET https://api.zukko.pro/v1/modelsor 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:
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:
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.
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-latestDetails: 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.
| Model | Formula per 1,000 functions | Total | For 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:
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.
Related articles
- Translating subtitles with a neural network: SRT without broken timecodesThe main risk when translating SRT with a neural network is a broken file structure. A scheme where timecodes never reach the model, and a Python script with validation.
- Editing texts with a neural network: proofreading, style and cuttingDifferent kinds of editing need different prompts. Wording for proofreading, style and cutting, how to get edits with explanations and how not to lose the author's voice.
- A neural network for writing scripts: Bash, PowerShell and PythonA model writes scripts for routine fast, but running them blindly is risky. How to state the task, demand a safe dry-run mode and check the result before launch.
- Dify in Docker: installation, first-run setup and connecting modelsInstall Dify with Docker Compose, create the admin account and connect Claude, GPT and DeepSeek models on one key. Plus what to do when containers will not start.