PyCodeCommenter — Python Docstring Generator & Validator
PyCodeCommenter writes and checks Python docstrings. It reads your code to fill in everything the code itself can prove — parameter names, types, defaults, when an exception is raised, what a boolean function tests — keeps anything you already wrote, and clearly marks what only a person can say. If you opt in, an AI model drafts those parts too, each line labelled for review. It also validates existing docstrings against the code and measures documentation coverage.
Install:
pip install pycodecommenter· Python 3.10+ · MIT License
See It Work
Given this file (no docstrings, one comment):
# Calculate what the customer owes after VAT and any discount.
def total_due(invoice_id: str, discount: float = 0.0) -> float:
invoice = INVOICES.get(invoice_id)
if invoice is None:
raise KeyError(invoice_id)
return invoice.amount * 1.18 - discount
def is_overdue(invoice, today) -> bool:
return today > invoice.due_date
pycodecommenter generate billing.py writes (real output):
# Calculate what the customer owes after VAT and any discount.
def total_due(invoice_id: str, discount: float = 0.0) -> float:
"""Calculate what the customer owes after VAT and any discount.
Args:
invoice_id (str): Unique identifier for the invoice.
discount (float): float value. (default: 0.0)
Returns:
float: TODO(pycodecommenter): describe
Raises:
KeyError: If `invoice is None`.
"""
...
def is_overdue(invoice, today) -> bool:
"""Is overdue.
Args:
invoice (Any): TODO(pycodecommenter): describe.
today (Any): TODO(pycodecommenter): describe.
Returns:
bool: True if `today > invoice.due_date`, otherwise False.
"""
...
The summary comes from your comment, which stays where it is. The raise condition and the boolean check are read straight from the code. Nothing is guessed: what the code can't say is marked TODO. Every run ends with a summary:
Summary: 2 docstrings written.
3 details taken straight from the code
1 docstring taken from the comment above it (comment left in place)
3 gaps left as "TODO(pycodecommenter)" for you to fill
Next: fill the gaps with `pycodecommenter review`, or add --ai-draft to have them drafted; then run `pycodecommenter validate`.
Add --ai-draft and the gaps are drafted, each line labelled (real output):
def is_overdue(invoice, today) -> bool:
"""Determine if an invoice is overdue. (AI-drafted, unreviewed)
Args:
invoice (Any): This is the invoice to check. (AI-drafted, unreviewed)
today (Any): This is today's date. (AI-drafted, unreviewed)
Returns:
bool: True if `today > invoice.due_date`, otherwise False.
"""
Then pycodecommenter review billing.py goes through each drafted line: accept it, edit it, or skip it.
What PyCodeCommenter Does
| Task | Command |
|---|---|
| Write missing docstrings (preview first) | pycodecommenter generate app.py --dry-run |
| Have AI draft what the code can't state | pycodecommenter generate app.py --ai-draft --dry-run |
| Accept, edit or fill drafts and gaps | pycodecommenter review app.py |
| Validate docstrings against the code | pycodecommenter validate app.py |
| Measure documentation coverage | pycodecommenter coverage ./src |
| JSON output for other tools | pycodecommenter validate app.py --output-format json |
It also catches documentation drift — code that changed while its docstring didn't: undocumented parameters, missing Returns: sections, entries for parameters that no longer exist, and mismatched types.
Quick Start
pip install pycodecommenter
# Preview what will be written (changes nothing)
pycodecommenter generate app.py --dry-run
# Write it
pycodecommenter generate app.py --inplace
# Optional: have AI draft the gaps, then go through the drafts
pycodecommenter generate app.py --ai-draft --dry-run
pycodecommenter review app.py
# Keep docstrings accurate, e.g. in CI
pycodecommenter validate app.py
pycodecommenter coverage ./src
Installing the development version
To work on PyCodeCommenter itself, or to try the unreleased code on main
(Python 3.10 or newer):
git clone https://github.com/AmosQuety/PyCodeCommenter.git
cd PyCodeCommenter
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest
pytest runs the test suite and the package's doctests; none of it needs a
network connection or an AI SDK. Before sending a change, also run black .
and flake8. See CONTRIBUTING.md for the full workflow
(also on the documentation site).
Why PyCodeCommenter?
The Problem
- Code changes quickly; docstrings lag behind and become stale.
- Few tools check existing docstrings against the real signatures.
- Tools that write docstrings for you tend to state guesses as if they were facts.
- Most projects have no measure of how well they're documented.
The Solution
- Facts first. Names, types, defaults, raise conditions and boolean checks are read from the code, so they're always right.
- Your words are never overwritten. Existing summaries, descriptions and entries are kept, a
#comment above an undocumented function becomes its docstring, and NumPy- or Sphinx-style docstrings keep their style. - Gaps are marked, not guessed. What the code can't state is left as
TODO(pycodecommenter): describe— or, with--ai-draft, drafted by an AI model and labelled(AI-drafted, unreviewed)until you review it. - Validation and coverage catch drift and measure progress, with JSON output for CI.
Related Tools
PyCodeCommenter isn't alone in this space. pydoclint checks Args/Returns/Yields/Raises against the actual function signature across Google, NumPy, and Sphinx styles, is actively maintained, and is fast — if you only need validation, it's a strong choice. interrogate measures presence-only documentation coverage, the same metric shape as coverage.py here. PyCodeCommenter's actual differentiator is breadth in one place: it's the only one of the three that generates a docstring skeleton and validates and measures coverage, all through one importable Python API — not just a CLI or pre-commit hook.
Features
Six Validation Checks
Every documented function is checked for:
- Signature Matching — every parameter in the function signature must appear in
Args:, and vice versa. - Type Consistency — type annotations must match documented types.
- Exception Documentation —
raisestatements require aRaises:section (Google or Sphinx style). - Return Documentation —
return <value>requires aReturns:section. - Format Compliance — docstring must have a summary line; non-standard section headers are flagged.
- Content Quality — placeholder text (
TODO,FIXME,Description of), short summaries, and duplicate descriptions are caught.
Decorator-Aware Validation (v2.2.0)
@propertygetter — return check fires as normal.@propertysetter / deleter — return check is skipped (no false-positive warnings).@classmethod—clsis excluded from parameter checks.@staticmethod— no self/cls stripping; all parameters validated.
Structured JSON Output (v2.2.0)
Both validate and coverage support --output-format json for machine-readable output:
pycodecommenter validate src/api.py --output-format json
{
"file": "src/api.py",
"stats": {
"total": 3,
"errors": 1,
"warnings": 2,
"info": 0,
"coverage_percentage": 85.0
},
"issues": [
{
"line": 42,
"severity": "ERROR",
"check": "signature",
"message": "Parameter 'timeout' is not documented in docstring"
}
]
}
Modern Python Support
- Python 3.10, 3.11, 3.12, 3.13.
async deffunctions.- Complex type hints:
Union,Optional,Generic,list[int],int | str. - PEP 604 unions, PEP 585 generics.
Coverage Reporting
- Per-file coverage percentages.
- Project-wide totals.
- JSON, Markdown, and console output.
- CI/CD integration with exit codes.
AI Drafting (Optional)
Some parts of a docstring can't be read off the code: what a function is for, or what an untyped argument means. By default PyCodeCommenter leaves those as TODO(pycodecommenter): describe markers. Add --ai-draft and an AI model drafts them instead:
pycodecommenter generate app.py --ai-draft --dry-run
- Only the gaps. Your own docstring text and facts read off the code (types, raise conditions) are never replaced; the model only fills parts that would otherwise be a TODO or say nothing beyond the type. That covers functions (summary, arguments, return, exceptions) and classes (summary and
Attributes:). - Every drafted line is labelled
(AI-drafted, unreviewed). The label stays until a person accepts the line inpycodecommenter review(review --listshows what is waiting), andpycodecommenter validatereports those lines until then. - Review first.
--dry-runand--output-dirshow the result without touching your files; writing with--inplacealso requires--accept-ai-drafts. - Consent first. Before any code is sent anywhere, you're asked once per destination (
--yes-send-code-to-aifor CI). What is sent is the source of each function with gaps and an outline of each such class, comments included; anything that looks like a key, password or token is left out, but check your comments hold no secrets. See Data and Privacy. - Know the cost first. For a directory, the tool counts the requests it would make (
32 files, 121 functions and classes have gaps to draft) and asks before sending;--max-drafts Ncaps the whole run.
Providers and models
By default drafts come from PyCodeCommenter's free hosted service: no key needed (the service does not store your code, but it forwards it to Google's Gemini API on a free tier, and Google may use content sent on that tier to improve its products; use your own key if that is not acceptable), with a daily limit (25 drafts per caller per day in v2.6.0; the service sets the figure, and each run ends with a line such as Hosted AI drafts left today: 10 of 25., which is the number to trust). The day is a UTC day, so it resets at midnight UTC; the count is kept in the service's memory, so a restart of the service can also reset it. When the limit is reached mid-run you're asked whether to continue with your own key. You can also start with your own key:
--ai-provider |
Key read from | Install | Default model |
|---|---|---|---|
hosted (default) |
— | included | chosen by the service |
gemini |
GEMINI_API_KEY |
pip install "pycodecommenter[gemini]" |
gemini-3.8-flash, then gemini-3.5-flash-lite, then gemini-2.5-flash if the earlier ones are unavailable to your key |
openai |
OPENAI_API_KEY |
pip install "pycodecommenter[openai]" |
gpt-6-luna |
anthropic |
ANTHROPIC_API_KEY |
pip install "pycodecommenter[anthropic]" |
claude-sonnet-5-5 |
deepseek |
DEEPSEEK_API_KEY |
pip install "pycodecommenter[openai]" |
deepseek-flash |
openai-compatible |
OPENAI_COMPATIBLE_API_KEY |
pip install "pycodecommenter[openai]" |
none: pass --ai-model and --ai-base-url |
If the provider's SDK isn't installed, PyCodeCommenter says so before it asks for consent or a key, and prints the exact command to install it into the Python you are running (it never runs pip itself). During a run, if you choose your own key after the hosted limit and that provider can't be used, you are asked again until one works or you type skip.
How each provider has been tested. The automated tests run every provider against fake SDK clients and make no network calls, so they check what PyCodeCommenter sends and how it handles replies and errors, not what a vendor's service accepts today. Live runs so far: the hosted service and Gemini have been used end to end, and Anthropic was run with claude-haiku-4-5-20251001 (its current default, claude-sonnet-5-5, has not been run live). OpenAI and DeepSeek have not been run against real keys; their request shapes and default models follow each vendor's documentation as of September 2026. If a request is rejected, that function keeps its TODO(pycodecommenter) marker (or, for a bad key or a spent quota, drafting stops for the run) and no code is changed. Please open an issue if a provider or model does not work for you.
The default model is only a default. Pass --ai-model to use any model your key can access, for example a more capable one:
pycodecommenter generate app.py --ai-draft --ai-provider anthropic --ai-model claude-opus-5-5 --dry-run
Every run prints the provider and model it is using.
Why a TODO can still remain
--ai-draft is meant to leave no TODO(pycodecommenter) behind, and the run summary says why any are left: the model declined a part (the code did not make it clear, and it would rather say nothing than guess), a request failed, drafting stopped (a spent limit, or --max-drafts), or the function's source looked like it holds a secret and was not sent. Fill what is left with pycodecommenter review, or run again. To check that nothing is unfinished, use pycodecommenter coverage --strict (counts only docstrings with no placeholder and no unreviewed AI line) and pycodecommenter validate --fail-on-todo.
Setting your API key
Set the provider's variable in the shell before running (the names are in the table above). For the current terminal session only:
# bash / zsh
export ANTHROPIC_API_KEY="your-key"
# PowerShell
$env:ANTHROPIC_API_KEY = "your-key"
.envfiles are not read. Export the variable yourself, or load the file in your own shell before running.- No variable set? In a terminal you are asked for the key with the input hidden. That key is used for this run only and is never saved; the tool never reads or writes keys in project files.
- Without a terminal (CI, a redirect) there is no prompt, so the variable must be set.
What it costs
Each function that still has gaps is one request to the provider, so a run over a large project makes many requests. With your own key the requests are billed by the provider. Bigger models cost more per request. The Anthropic default, claude-sonnet-5-5, is a mid-priced model asked for low effort, which is enough for one-sentence docstrings; a smaller one such as claude-haiku-4-5-20251001 costs less, and a larger one such as claude-opus-5-5 costs more, both chosen with --ai-model. The OpenAI default, gpt-6-luna, is that family's cheapest model and is asked for low reasoning effort. Check your provider's price list before a large run. Try --dry-run on one file first.
Usage Examples
Example 1: Generate Docstrings
from PyCodeCommenter import PyCodeCommenter
code = """
def calculate_discount(price: float, rate: float = 0.1) -> float:
return price * (1 - rate)
"""
commenter = PyCodeCommenter().from_string(code)
print(commenter.get_patched_code())
Output (real):
def calculate_discount(price: float, rate: float = 0.1) -> float:
"""Calculate discount.
Args:
price (float): float value.
rate (float): float value. (default: 0.1)
Returns:
float: TODO(pycodecommenter): describe
"""
return price * (1 - rate)
The types and default are facts from the signature; "float value." says no more than the type, and the return value is marked for a person to describe. With --ai-draft (CLI) or PyCodeCommenter(description_provider=...) (API), those parts are drafted instead and labelled (AI-drafted, unreviewed).
Review what was drafted
pycodecommenter review app.py
Steps through every AI-drafted line (accept, edit or skip), every TODO gap (fill or skip), and every comment a new docstring now repeats (remove only if you say yes). Only docstrings and approved comments change, and a file is saved only if its code is exactly the same as before. --list just lists them.
Example 2: Validate in CI/CD
import sys
from PyCodeCommenter import PyCodeCommenter
commenter = PyCodeCommenter().from_file("src/main.py")
report = commenter.validate()
if report.stats.errors > 0:
report.print_summary()
sys.exit(1) # Fail the CI build
print(f"✓ Documentation validated: {report.stats.coverage_percentage:.1f}% coverage")
Example 3: Enforce Coverage Threshold
from PyCodeCommenter import CoverageAnalyzer
analyzer = CoverageAnalyzer()
project = analyzer.analyze_directory("./src", exclude_patterns=["tests"])
if project.total_coverage < 80.0:
print(f"❌ Coverage {project.total_coverage:.1f}% is below the 80% threshold")
project.print_report()
sys.exit(1)
print(f"✓ Coverage {project.total_coverage:.1f}% meets the threshold")
Example 4: JSON Export
import json
from PyCodeCommenter import PyCodeCommenter
commenter = PyCodeCommenter().from_file("mycode.py")
report = commenter.validate()
with open("validation_report.json", "w") as f:
json.dump(report.to_dict(), f, indent=2)
CI/CD Integration
GitHub Actions
# .github/workflows/docs.yml
name: Documentation Check
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install pycodecommenter
- run: pycodecommenter validate src/
Pre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: validate-docstrings
name: Validate Docstrings
entry: pycodecommenter validate
language: system
types: [python]
Frequently Asked Questions
What is PyCodeCommenter? PyCodeCommenter is a Python command-line tool and library for automatically generating Google-style docstrings and validating existing docstrings against real function signatures.
How do I install PyCodeCommenter?
Run pip install pycodecommenter. Python 3.10 or later is required.
Does PyCodeCommenter use AI or LLMs?
Not unless you ask it to. By default it is fully deterministic: it uses Python's built-in ast module, makes no network requests, and gives the same output for the same input. With --ai-draft, an AI model drafts only the parts the code can't state, every drafted line is labelled (AI-drafted, unreviewed), and nothing is sent without your consent. See AI Drafting (Optional).
Does PyCodeCommenter overwrite my hand-written docstrings? No. Existing summaries and parameter, return, exception and attribute descriptions are preserved and merged. Only missing sections are filled in automatically.
What docstring styles does PyCodeCommenter support?
New docstrings are Google style. Existing NumPy-style (dash-underlined Parameters/Returns/Raises) and Sphinx-style (:param:, :type:, :returns:, :raises:) docstrings keep their style: gaps are filled in the same convention, and nothing is converted.
Can I use PyCodeCommenter in CI/CD?
Yes. The validate subcommand exits with code 1 when any ERROR-level issue is found, making it suitable for blocking CI builds. The --output-format json flag enables integration with any downstream tooling.
What is documentation drift? Documentation drift is when code is updated but the corresponding docstrings are not. Parameters get added or renamed, return types change, and exceptions get added — but the docstring stays the same. PyCodeCommenter detects and fixes this.
How does PyCodeCommenter measure documentation coverage?
Coverage is (documented_functions + documented_classes) / (total_functions + total_classes) × 100. A function counts as documented if its first body statement is a non-empty string literal.
Configuration
Create .pycodecommenter.yaml in your project root:
style: google
validation:
level: strict
check_types: true
check_exceptions: true
coverage:
threshold: 80
fail_below: true
exclude:
- "*/tests/*"
- "*/migrations/*"
- "*/__pycache__/*"
Documentation
Full documentation: https://amosquety.github.io/PyCodeCommenter/
Supported Platforms & Environments
- OS: Linux, macOS, Windows
- Python: 3.10, 3.11, 3.12, 3.13
- Environments: local, CI/CD (GitHub Actions, GitLab CI, Jenkins), pre-commit hooks
- Dependencies:
ruamel.yaml(config files),libcst(docstring patching); AI provider SDKs only as optional extras ([gemini],[openai],[anthropic])
Known Limitations
- Python 2.x is not supported (EOL).
matchstatements (Python 3.10+) have basic support.- Without
--ai-draft, prose that can't be extracted from the AST (what a parameter or function means, as opposed to its name/type/default) is a marked placeholder,TODO(pycodecommenter): describe, for a human to fill in. With--ai-draft, it is an AI draft labelled(AI-drafted, unreviewed)— still to be reviewed, never presented as finished documentation.
Roadmap
- VS Code extension
- Smart docstring updates that preserve human-written content
- Optional AI drafting (v2.6.0) — opt-in (
--ai-draft), fills only the gaps the code can't state, labels every drafted line(AI-drafted, unreviewed), review-first by default (--dry-run/--output-dir), and writing in place needs a separate--accept-ai-drafts - NumPy and full Sphinx style support (v2.3.0)
- GitHub Action for automated documentation PRs
-
--fail-belowflag for coverage threshold enforcement in CLI (v2.3.0)
Creator
PyCodeCommenter was created and is actively maintained by Nabasa Amos (Amos Quety), a software engineer focused on developer tooling, documentation automation, and software quality.
- Portfolio: nabasa-amos.netlify.app
- GitHub: AmosQuety
- LinkedIn: Nabasa Amos
- Contact: amosnabasa4@gmail.com
License
MIT License — see LICENSE for details.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md before submitting a pull request.
Issues & Discussions
- Bug reports: GitHub Issues
- Questions & ideas: GitHub Discussions
If PyCodeCommenter is useful in your workflow, a ⭐ on GitHub helps other Python developers discover it.
Release files for pycodecommenter 2.6.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pycodecommenter-2.6.1.tar.gz | 189.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pycodecommenter-2.6.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 318.5 kB
Release files / pycodecommenter-2.6.1.tar.gz
| Download URL | pycodecommenter-2.6.1.tar.gz |
|---|---|
| Size | 189.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
205073a3d6f005b9d0013e624ec1fedd87ed4e15621fb1244557f3e00ce7d560
|
|
BLAKE2b-256 checksum How to use checksums |
69df45ec032f23d9b25501cec032f0883205ac2dc6ffa03c8adecbc48c71dff4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logRelease files / pycodecommenter-2.6.1-py3-none-any.whl
| Download URL | pycodecommenter-2.6.1-py3-none-any.whl |
|---|---|
| Size | 129.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b71316abea33facb22b14760b6fe8b97c4dadda9b7e2ed1bd361817c451fb62f
|
|
BLAKE2b-256 checksum How to use checksums |
e885a31570879073b36338cf172f549d5469342cc9f7b97e932ffcc1362c4d0b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency log