A Python docstring linter with auto-fix support
Project description
pydocfix
A Python docstring linter that checks signature ↔ docstring consistency and auto-fixes violations.
Inspired by pydoclint, pydocfix goes further by automatically repairing the issues it finds.
[!NOTE] This project is in beta (v0.1.0b3). APIs and behavior may change before the stable release.
Table of Contents
- Why pydocfix?
- Installation
- Quick Start
- Configuration
- Rules
- Suppressing violations
- Baseline
- pre-commit
- Benchmark
- Custom rules
- License
Why pydocfix?
pydoclint pioneered fast signature ↔ docstring consistency checking for Python. However, it can only report violations — all corrections must be done by hand.
pydocfix is built on pydocstring-rs, a CST (Concrete Syntax Tree) parser for docstrings written in Rust by the same author. CST preserves every token's byte offset, whitespace, and formatting, enabling:
- Byte-level diagnostics — point to the exact token (parameter name, type annotation, section header), not just the line
- Surgical auto-fix — edits replace precise byte ranges, so fixes never corrupt adjacent content
- Iterative fix loop — apply non-overlapping fixes, re-parse, repeat until stable
Feature comparison
| pydocfix | pydoclint | |
|---|---|---|
| Auto-fix (safe + unsafe) | ✅ | - |
| Google style | ✅ | ✅ |
| NumPy style | ✅ | ✅ |
| Sphinx style | - | ✅ |
| Parameter checking | ✅ | ✅ |
| Return type checking | ✅ | ✅ |
| Yield checking | ✅ | ✅ |
| Raises checking | ✅ | ✅ |
Class docstring / __init__ rules |
✅ | ✅ |
| Class attribute checking | - | ✅ |
Default value checking (optional / default) |
✅ | - |
| Byte-level diagnostics | ✅ | - |
| Baseline suppression | ✅ | ✅ |
Inline # noqa |
✅ | ✅ |
| flake8 plugin | - | ✅ |
| pre-commit hook | ✅ | ✅ |
| Parallel execution | ✅ | - |
Example
Given this function (docs/tapes/example.py) where the docstring says int but the signature says float:
def foo(x: float) -> float:
"""Do something with x.
Args:
x (int): The input value.
Returns:
float: The result.
"""
return x
pydocfix points to the exact token — the 3-char int inside x (int), and --diff previews the surgical fix:
Apply with pydocfix check example.py --fix --unsafe-fixes to update int → float.
Installation
pip install pydocfix
Or with uv:
uv add pydocfix # add to a project
uv tool install pydocfix # install as a standalone tool
Requires Python 3.11+.
Quick Start
# Check docstrings (report only)
pydocfix check src/
# Show diff of proposed fixes
pydocfix check src/ --diff
# Apply safe fixes
pydocfix check src/ --fix
# Apply safe + unsafe fixes
pydocfix check src/ --fix --unsafe-fixes
# Select / ignore specific rules or categories
pydocfix check src/ --select PRM --ignore RTN,YLD
# Parallel execution (auto-detected for ≥8 files; override with --jobs)
pydocfix check src/ --jobs 4
# Concise (single-line) output
pydocfix check src/ --output-format concise
# Disable color output
pydocfix check src/ --no-color
Configuration
Configure via pyproject.toml:
[tool.pydocfix]
# Rule selection (see "Rule selectors" for syntax details)
select = ["ALL"]
ignore = ["RIS"]
extend-safe-fixes = ["PRM"]
extend-unsafe-fixes = ["RTN", "YLD"]
# Type annotation style: "signature" | "docstring" | "both" | omitted (default)
# omitted — PRM103/RTN103/YLD103 and PRM104/RTN104/YLD104 are all disabled
# signature — redundant docstring types flagged (x104); missing signature annotations flagged (x105)
# docstring — missing docstring types flagged (x103); redundant signature annotations flagged (x106)
# both — missing docstring types flagged (x103); missing signature annotations flagged (x105)
type_annotation_style = "signature"
# Preferred docstring style: "google" (default) | "numpy"
# Controls the format of auto-generated sections for plain (summary-only) docstrings.
# Existing Google/NumPy-style docstrings are always fixed in their detected style.
preferred_style = "google"
# Where __init__ arguments and raises should be documented.
# PRM001/RIS001 never fire on __init__; use CLS rules instead.
# omitted — CLS103–CLS106 and CLS203–CLS206 are disabled; __init__ is not checked (default)
# "class" — Args/Raises belong in the class docstring (CLS105/CLS106 enforce presence)
# "init" — Args/Raises belong in the __init__ docstring (CLS205/CLS206 enforce presence)
# "both" — Args/Raises may appear in either; presence/absence not enforced
class-docstring-style = "class"
# Paths/patterns to exclude (in addition to built-in defaults).
# Supports:
# - Simple names: matched against each directory's name (e.g. "build", ".venv")
# - Glob patterns: matched against paths relative to the project root
# - "*" matches any sequence of characters except "/"
# - "**" matches zero or more path components
# Examples:
# "tests/" — exclude the top-level tests directory
# "tests/**/fixtures/" — exclude every fixtures/ directory under tests/
# "src/generated_*.py" — exclude files matching the glob
exclude = ["tests/", "docs/"]
# Skip section-level rules (PRM001, RTN001, YLD001, RIS001) for one-line docstrings (default: true)
skip_short_docstrings = true
# Treat Optional[T], T | None, and Union[T, None] as equivalent to T
# when comparing types in PRM101/RTN101/YLD101 (default: false)
allow_optional_shorthand = false
# Path to the baseline file (relative to pyproject.toml)
baseline = ".pydocfix-baseline.json"
# Output format: "full" (default, with source context) | "concise" (single-line)
output-format = "full"
Rules
Each rule is classified as safe fix, unsafe fix, or report-only.
- Safe fixes can be applied automatically with
--fix(no risk of changing semantics) - Unsafe fixes require
--fix --unsafe-fixes(may alter docstring meaning)
✅ in the Default column means the rule is enabled by default.
Rule selectors
--select, --ignore, --extend-safe-fixes, --extend-unsafe-fixes (CLI) and their pyproject.toml equivalents all accept the same rule selector syntax:
| Format | Example | Matches |
|---|---|---|
| Exact code | PRM001 |
PRM001 only |
| Category prefix | PRM |
All PRM rules |
ALL |
ALL |
Every rule |
Summary (SUM)
| Code | Default | Fix | Description |
|---|---|---|---|
| SUM001 | ✅ | - | Missing summary line |
| SUM002 | ✅ | safe | Summary doesn't end with period |
Parameters (PRM)
| Code | Default | Fix | Description |
|---|---|---|---|
| PRM001 | ✅ | unsafe | Missing Args/Parameters section |
| PRM002 | ✅ | safe | Unnecessary Args/Parameters section |
| PRM003 | ✅ | safe | self/cls documented in docstring |
| PRM004 | ✅ | unsafe | Parameter in signature missing from docstring |
| PRM005 | ✅ | unsafe | Parameter in docstring not in signature |
| PRM006 | ✅ | unsafe | Parameter order mismatch |
| PRM007 | ✅ | unsafe | Duplicate parameter name |
| PRM008 | ✅ | - | Parameter has no description |
| PRM009 | ✅ | safe | Missing */** prefix on *args/**kwargs |
| PRM101 | ✅ | unsafe | Docstring type doesn't match signature annotation |
| PRM102 | ✅ | unsafe | No type in docstring or signature |
| PRM103 | unsafe | No type in docstring | |
| PRM104 | safe | Redundant type in docstring (signature has annotation) | |
| PRM105 | - | No type annotation in signature (type_annotation_style = "signature" or "both") |
|
| PRM106 | - | Redundant type annotation in signature (type_annotation_style = "docstring") |
|
| PRM201 | ✅ | unsafe | Missing optional for parameter with default |
| PRM202 | unsafe | Missing default for parameter with default |
Returns (RTN)
| Code | Default | Fix | Description |
|---|---|---|---|
| RTN001 | ✅ | unsafe | Missing Returns section |
| RTN002 | ✅ | safe | Unnecessary Returns section |
| RTN003 | ✅ | - | Returns entry has no description |
| RTN101 | ✅ | unsafe | Return type mismatch |
| RTN102 | ✅ | unsafe | No return type anywhere |
| RTN103 | unsafe | No return type in docstring | |
| RTN104 | safe | Redundant return type in docstring | |
| RTN105 | - | No return type annotation in signature (type_annotation_style = "signature" or "both") |
|
| RTN106 | - | Redundant return type annotation in signature (type_annotation_style = "docstring") |
Yields (YLD)
| Code | Default | Fix | Description |
|---|---|---|---|
| YLD001 | ✅ | unsafe | Missing Yields section |
| YLD002 | ✅ | safe | Unnecessary Yields section |
| YLD003 | ✅ | - | Yields entry has no description |
| YLD101 | ✅ | unsafe | Yield type mismatch |
| YLD102 | ✅ | unsafe | No yield type anywhere |
| YLD103 | unsafe | No yield type in docstring | |
| YLD104 | safe | Redundant yield type in docstring | |
| YLD105 | - | No yield type annotation in signature (type_annotation_style = "signature" or "both") |
|
| YLD106 | - | Redundant yield type annotation in signature (type_annotation_style = "docstring") |
Raises (RIS)
| Code | Default | Fix | Description |
|---|---|---|---|
| RIS001 | ✅ | unsafe | Missing Raises section |
| RIS002 | ✅ | safe | Unnecessary Raises section |
| RIS003 | ✅ | - | Raises entry has no description |
| RIS004 | ✅ | unsafe | Raised exception not documented |
| RIS005 | ✅ | unsafe | Documented exception not raised |
Docstring (DOC)
| Code | Default | Fix | Description |
|---|---|---|---|
| DOC001 | ✅ | unsafe | Section order doesn't match convention |
| DOC002 | ✅ | safe | Incorrect indentation of a docstring section entry |
| DOC003 | ✅ | safe | One-line docstring should be written on a single line |
Class (CLS)
| Code | Default | Fix | Description |
|---|---|---|---|
| CLS001 | ✅ | - | __init__ has its own docstring but the class also has one |
| CLS101 | ✅ | safe | Class docstring has a Returns section |
| CLS102 | ✅ | safe | Class docstring has a Yields section |
| CLS103 | unsafe | Class docstring has an Args section (class-docstring-style = "init") |
|
| CLS104 | unsafe | Class docstring has a Raises section (class-docstring-style = "init") |
|
| CLS105 | unsafe | Class docstring missing Args section (class-docstring-style = "class") |
|
| CLS106 | unsafe | Class docstring missing Raises section (class-docstring-style = "class") |
|
| CLS201 | ✅ | safe | __init__ docstring has a Returns section |
| CLS202 | ✅ | safe | __init__ docstring has a Yields section |
| CLS203 | unsafe | __init__ docstring has an Args section (class-docstring-style = "class") |
|
| CLS204 | unsafe | __init__ docstring has a Raises section (class-docstring-style = "class") |
|
| CLS205 | unsafe | __init__ docstring missing Args section (class-docstring-style = "init") |
|
| CLS206 | unsafe | __init__ docstring missing Raises section (class-docstring-style = "init") |
Note: PRM001 and RIS001 never fire on
__init__; use CLS rules to enforce__init__docstring conventions.
Suppressing violations
Inline suppression (# noqa)
Add a # noqa comment on the closing """ line to suppress violations for that docstring.
def foo(x):
"""Short summary.""" # noqa # suppress all rules for this docstring
def bar(x):
"""Short summary.""" # noqa: PRM001 # suppress only PRM001
def baz(x):
"""Short summary.""" # noqa: PRM001, RTN001 # suppress multiple rules
# For multiline docstrings, put the comment on the closing """ line
def qux(x: int) -> int:
"""Short summary.
Args:
x: A value.
""" # noqa: RTN001
return x
Unused # noqa codes are reported as NOQ001 (and removed by --fix).
File-level suppression
Put a # pydocfix: noqa comment on its own line anywhere in the file to suppress violations for every docstring in the file.
# pydocfix: noqa # suppress all rules in this file
# pydocfix: noqa: PRM001 # suppress only PRM001 in this file
Baseline
The baseline lets you record the current violation state of a project and suppress those existing violations on future runs — so only new violations are reported. This makes gradual adoption easier: fix violations at your own pace.
# Record all current violations as the baseline
pydocfix check src/ --baseline .pydocfix-baseline.json --generate-baseline
# Future runs only report violations not in the baseline
pydocfix check src/ --baseline .pydocfix-baseline.json
Or configure the baseline path in pyproject.toml so you don't need the flag every time:
[tool.pydocfix]
baseline = ".pydocfix-baseline.json"
Then generate and use it:
pydocfix check src/ --generate-baseline # write baseline
pydocfix check src/ # only new violations reported
The baseline file is a JSON file that records violations by symbol name (e.g. MyClass.my_method) rather than line number, so it stays stable when unrelated code is added or removed.
Fixed violations are automatically removed from the baseline on the next run.
pre-commit
Add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/ryumasai/pydocfix
rev: v0.1.0b3 # check https://pypi.org/project/pydocfix/ for the latest version
hooks:
- id: pydocfix
To enable auto-fix:
- id: pydocfix
args: [--fix]
Benchmark
pydocfix vs pydoclint
pydocfix performs linting and auto-fix generation in a single pass, yet is significantly faster than pydoclint (lint-only) thanks to parallel file processing:
Parallel (default, auto-detected cores — 10-core machine)
| Project | Files | Lines | pydocfix | pydoclint | Speedup |
|---|---|---|---|---|---|
| numpy | 425 | 252K | 0.74 sec | 2.90 sec | 3.9x |
| scikit-learn | 637 | 372K | 0.85 sec | 4.24 sec | 5.0x |
Single-threaded (--jobs 1)
| Project | Files | Lines | pydocfix | pydoclint | Speedup |
|---|---|---|---|---|---|
| numpy | 425 | 252K | 2.18 sec | 2.90 sec | 1.3x |
| scikit-learn | 637 | 372K | 2.43 sec | 4.24 sec | 1.7x |
Median of 5 runs (+ 1 warmup) via hyperfine. pydoclint runs single-threaded only. pydoclint configured with
--arg-type-hints-in-signature False --arg-type-hints-in-docstring Falseto match pydocfix's default (notype_annotation_styleset).
Violations detected
| Project | pydocfix | pydoclint |
|---|---|---|
| numpy | 2,562 | 2,787 |
| scikit-learn | 4,551 | 5,230 |
Custom rules
pydocfix supports custom rules via its plugin system. Plugins are plain Python modules that define rule functions using the @rule decorator.
# my_rules.py
from collections.abc import Iterator
from pydocstring import GoogleDocstring
from pydocfix.rules._base import FunctionCtx, make_diagnostic, rule
from pydocfix.diagnostics import Diagnostic
@rule(
"MY001",
ctx_types=frozenset({FunctionCtx}),
cst_types=frozenset({GoogleDocstring}),
)
def my001(node: GoogleDocstring, ctx: FunctionCtx) -> Iterator[Diagnostic]:
"""Require a non-empty summary."""
if node.summary is None:
yield make_diagnostic("MY001", ctx, "Missing summary")
Register plugins in pyproject.toml:
[tool.pydocfix]
plugin-modules = ["my_rules"] # importable module name
# plugin-paths = ["my_rules.py"] # or a file path relative to pyproject.toml
Plugins can also declare fix edits, accept per-rule config via [tool.pydocfix.plugin-config.<rule-id>], and use the same @rule options as built-in rules (conflicts_with, activation_condition, enabled_by_default, etc.).
See examples/custom_rules.py for a complete worked example.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydocfix-0.1.0b3.tar.gz.
File metadata
- Download URL: pydocfix-0.1.0b3.tar.gz
- Upload date:
- Size: 413.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d01071fdea34604271565f904ef2b42b7762ba141fbdc272bb6c9929704c226e
|
|
| MD5 |
d55b24e2a37d563e77cc546e1a2bad4c
|
|
| BLAKE2b-256 |
020a2b9df9cf981cf0bfb2e8c916fa0f07cf4c7560407719905570122475a776
|
Provenance
The following attestation bundles were made for pydocfix-0.1.0b3.tar.gz:
Publisher:
release.yml on ryumasai/pydocfix
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydocfix-0.1.0b3.tar.gz -
Subject digest:
d01071fdea34604271565f904ef2b42b7762ba141fbdc272bb6c9929704c226e - Sigstore transparency entry: 1347894399
- Sigstore integration time:
-
Permalink:
ryumasai/pydocfix@428ed8a82ad1057a9796973060f24889fce00f11 -
Branch / Tag:
refs/tags/v0.1.0b3 - Owner: https://github.com/ryumasai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@428ed8a82ad1057a9796973060f24889fce00f11 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pydocfix-0.1.0b3-py3-none-any.whl.
File metadata
- Download URL: pydocfix-0.1.0b3-py3-none-any.whl
- Upload date:
- Size: 99.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d04972f9d0473808836f3ad994c52a0fb332a733332192087e99b9289f0809c
|
|
| MD5 |
473fccd13b36e22aacf1fed27634181e
|
|
| BLAKE2b-256 |
3018a2ce5a3ab9702fc1b1947f1eea0b0b1ed48da4b80f1cc7aed636cf110911
|
Provenance
The following attestation bundles were made for pydocfix-0.1.0b3-py3-none-any.whl:
Publisher:
release.yml on ryumasai/pydocfix
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydocfix-0.1.0b3-py3-none-any.whl -
Subject digest:
4d04972f9d0473808836f3ad994c52a0fb332a733332192087e99b9289f0809c - Sigstore transparency entry: 1347894404
- Sigstore integration time:
-
Permalink:
ryumasai/pydocfix@428ed8a82ad1057a9796973060f24889fce00f11 -
Branch / Tag:
refs/tags/v0.1.0b3 - Owner: https://github.com/ryumasai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@428ed8a82ad1057a9796973060f24889fce00f11 -
Trigger Event:
push
-
Statement type: