Skip to content

[pydocstyle] Add rule D420 to enforce docstring section ordering#23537

Merged
amyreese merged 4 commits intoastral-sh:mainfrom
o1x3:pydocstyle/d420-section-order
Feb 25, 2026
Merged

[pydocstyle] Add rule D420 to enforce docstring section ordering#23537
amyreese merged 4 commits intoastral-sh:mainfrom
o1x3:pydocstyle/d420-section-order

Conversation

@o1x3
Copy link
Contributor

@o1x3 o1x3 commented Feb 24, 2026

Summary

Docstring sections like Parameters, Returns, Notes, Examples etc. have a canonical ordering defined by the convention. Contributors frequently get this wrong, putting Notes before Returns, or Examples before Parameters.

This PR adds a new preview rule D420 (SectionOrderIncorrect) that flags sections appearing out of order.

For NumPy, the full ordering from the numpydoc spec is enforced. For Google, only the constraints explicitly documented in the style guide are enforced (Args before Returns/Yields, Raises after those), all other sections are unordered. This was discussed in the issue with @ntBre.

PEP 257 convention ignores the rule. When no convention is set, it auto-detects the style, matching D405-D417 behavior.

No autofix, reordering sections risks mangling content between them.

Closes #9641

Test Plan

  • Added convention-specific snapshot tests for both NumPy and Google covering correct order, single/multiple swaps, alias sections (Other Params, Arguments, Keyword Args), class docstrings, and unrecognized sections
  • Google tests verify non-core sections (Notes, Examples, Attributes) produce no diagnostic regardless of order
  • Verified with cargo test -p ruff_linter, cargo clippy, and uvx prek run -a

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 24, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+510 -2 violations, +0 -0 fixes in 8 projects; 48 projects unchanged)

DisnakeDev/disnake (+230 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ disnake/abc.py:1117:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/abc.py:1362:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/abc.py:1402:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/abc.py:1676:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/abc.py:1872:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/abc.py:1915:9: D420 Section "Parameters" appears after section "Examples" but should be before it
+ disnake/abc.py:1926:9: D420 Section "Raises" appears after section "Examples" but should be before it
+ disnake/abc.py:1931:9: D420 Section "Yields" appears after section "Examples" but should be before it
+ disnake/abc.py:1969:9: D420 Section "Parameters" appears after section "Examples" but should be before it
+ disnake/abc.py:1993:9: D420 Section "Raises" appears after section "Examples" but should be before it
+ disnake/abc.py:2000:9: D420 Section "Yields" appears after section "Examples" but should be before it
+ disnake/abc.py:2073:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/abc.py:717:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/abc.py:976:9: D420 Section "Parameters" appears after section "Examples" but should be before it
+ disnake/abc.py:989:9: D420 Section "Raises" appears after section "Examples" but should be before it
+ disnake/appinfo.py:549:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/asset.py:149:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/asset.py:428:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/asset.py:480:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/asset.py:508:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/asset.py:546:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ disnake/asset.py:56:9: D420 Section "Returns" appears after section "Raises" but should be before it
... 208 additional changes omitted for project

RasaHQ/rasa (+9 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ .github/scripts/mr_publish_results.py:99:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ rasa/core/actions/action.py:177:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ rasa/graph_components/validators/finetuning_validator.py:95:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ rasa/model_testing.py:403:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ rasa/model_training.py:113:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ rasa/shared/nlu/training_data/entities_parser.py:167:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ rasa/shared/utils/io.py:595:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ rasa/shared/utils/io.py:611:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ rasa/utils/tensorflow/model_data.py:976:9: D420 Section "Returns" appears after section "Raises" but should be before it

PlasmaPy/PlasmaPy (+22 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ src/plasmapy/formulary/collisions/coulomb.py:439:5: D420 Section "See Also" appears after section "Examples" but should be before it
+ src/plasmapy/formulary/collisions/frequencies.py:1081:5: D420 Section "See Also" appears after section "Examples" but should be before it
+ src/plasmapy/formulary/collisions/frequencies.py:143:5: D420 Section "See Also" appears after section "Examples" but should be before it
+ src/plasmapy/formulary/collisions/frequencies.py:732:5: D420 Section "See Also" appears after section "Examples" but should be before it
+ src/plasmapy/formulary/collisions/frequencies.py:917:5: D420 Section "See Also" appears after section "Examples" but should be before it
+ src/plasmapy/particles/atomic.py:534:5: D420 Section "See Also" appears after section "Notes" but should be before it
+ src/plasmapy/particles/atomic.py:635:5: D420 Section "See Also" appears after section "Notes" but should be before it
+ src/plasmapy/particles/atomic.py:755:5: D420 Section "See Also" appears after section "Notes" but should be before it
+ src/plasmapy/particles/decorators.py:430:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ src/plasmapy/particles/ionization_state_collection.py:117:5: D420 Section "Notes" appears after section "Examples" but should be before it
... 12 additional changes omitted for project

apache/airflow (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ providers/standard/src/airflow/providers/standard/operators/hitl.py:264:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ task-sdk/src/airflow/sdk/io/path.py:255:9: D420 Section "See Also" appears after section "Examples" but should be before it

ibis-project/ibis (+57 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ ibis/backends/__init__.py:1045:9: D420 Section "Returns" appears after section "Notes" but should be before it
+ ibis/backends/duckdb/__init__.py:1160:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ ibis/backends/materialize/__init__.py:173:9: D420 Section "See Also" appears after section "Examples" but should be before it
+ ibis/backends/materialize/__init__.py:208:9: D420 Section "See Also" appears after section "Examples" but should be before it
+ ibis/backends/materialize/__init__.py:2234:9: D420 Section "Notes" appears after section "Examples" but should be before it
+ ibis/backends/materialize/api.py:144:5: D420 Section "Notes" appears after section "Examples" but should be before it
+ ibis/backends/materialize/api.py:150:5: D420 Section "References" appears after section "Examples" but should be before it
+ ibis/backends/materialize/api.py:49:5: D420 Section "See Also" appears after section "Examples" but should be before it
+ ibis/backends/materialize/api.py:53:5: D420 Section "Notes" appears after section "Examples" but should be before it
+ ibis/backends/materialize/api.py:67:5: D420 Section "References" appears after section "Examples" but should be before it
... 47 additional changes omitted for project

langchain-ai/langchain (+42 -2 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ libs/core/langchain_core/_import_utils.py:25:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ libs/core/langchain_core/callbacks/manager.py:2323:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ libs/core/langchain_core/document_loaders/base.py:71:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ libs/core/langchain_core/documents/base.py:164:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ libs/core/langchain_core/documents/base.py:182:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ libs/core/langchain_core/documents/base.py:201:9: D420 Section "Yields" appears after section "Raises" but should be before it
... 35 additional changes omitted for rule D420
+ libs/core/langchain_core/language_models/llms.py:132:5: DOC201 `return` is not documented in docstring
- libs/core/langchain_core/language_models/llms.py:132:5: DOC201 `return` is not documented in docstring
+ libs/core/langchain_core/messages/utils.py:69:5: DOC201 `return` is not documented in docstring
- libs/core/langchain_core/messages/utils.py:69:5: DOC201 `return` is not documented in docstring
... 34 additional changes omitted for project

reflex-dev/reflex (+35 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ reflex/app.py:1764:5: D420 Section "Yields" appears after section "Raises" but should be before it
+ reflex/app.py:2155:9: D420 Section "Args" appears after section "Raises" but should be before it
+ reflex/app.py:615:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ reflex/app.py:929:9: D420 Section "Args" appears after section "Raises" but should be before it
+ reflex/assets.py:50:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ reflex/compiler/utils.py:111:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ reflex/compiler/utils.py:45:5: D420 Section "Returns" appears after section "Raises" but should be before it
+ reflex/components/base/meta.py:20:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ reflex/components/component.py:1172:9: D420 Section "Returns" appears after section "Raises" but should be before it
+ reflex/components/core/breakpoints.py:69:9: D420 Section "Returns" appears after section "Raises" but should be before it
... 25 additional changes omitted for project

astropy/astropy (+113 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ astropy/coordinates/attributes.py:51:5: D420 Section "Parameters" appears after section "Examples" but should be before it
+ astropy/coordinates/earth.py:385:9: D420 Section "See Also" appears after section "Examples" but should be before it
+ astropy/coordinates/sky_coordinate.py:1029:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ astropy/coordinates/sky_coordinate.py:1080:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ astropy/coordinates/sky_coordinate.py:1121:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ astropy/coordinates/sky_coordinate.py:1175:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ astropy/coordinates/sky_coordinate.py:1233:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ astropy/coordinates/sky_coordinate.py:123:5: D420 Section "Parameters" appears after section "Examples" but should be before it
+ astropy/coordinates/sky_coordinate.py:1295:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ astropy/coordinates/sky_coordinate.py:1346:9: D420 Section "See Also" appears after section "Notes" but should be before it
+ astropy/coordinates/sky_coordinate.py:1405:9: D420 Section "See Also" appears after section "Notes" but should be before it
... 102 additional changes omitted for project

... Truncated remaining completed project reports due to GitHub comment length restrictions

Changes by rule (2 rules affected)

code total + violation - violation + fix - fix
D420 508 508 0 0 0
DOC201 4 2 2 0 0

@ntBre ntBre self-requested a review February 24, 2026 15:57
@ntBre ntBre added rule Implementing or modifying a lint rule docstring Related to docstring linting or formatting preview Related to preview mode features labels Feb 24, 2026
@amyreese amyreese self-assigned this Feb 25, 2026
@amyreese amyreese self-requested a review February 25, 2026 01:18
Copy link
Member

@amyreese amyreese left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at ecosystem results, there are some cases where this fires on private (prefixed with _) functions, or functions that don't have any marked sections at all. Both cases feel like they should be ignored by this rule, and we should have test cases covering them: https://github.com/langchain-ai/langchain/blob/4ffb584ddf09440184dda63d11187b0bdd2b63b2/libs/core/langchain_core/messages/utils.py#L69

- Fix let-chain syntax (stray `if` in commit suggestion)
- Move SectionOrderIncorrect struct up with other violation structs
- Combine tests with #[test_case] and explicit snapshot names
- Add edge case tests: private functions, no-section docstrings, section-like prose
@o1x3 o1x3 force-pushed the pydocstyle/d420-section-order branch from bd8a1d6 to 654a763 Compare February 25, 2026 09:32
@o1x3
Copy link
Contributor Author

o1x3 commented Feb 25, 2026

Thanks for the review @amyreese

  • Fixed the let-chain syntax (the suggestion had a stray if)
  • Moved the rule struct up with the other violation structs
  • Combined tests with #[test_case]
  • Added test cases for no-section docstrings and section-like words in prose (both correctly produce zero violations)
  • Added a _private_function_out_of_order test case. D420 fires on it, same as other section rules (D405-D417) which don't skip private defs. The rule only triggers when 2+ recognized sections are out of order, so docstrings without sections are unaffected.

Regarding the linked langchain file (messages/utils.py:69): that line is a DOC201 change in the ecosystem diff, not D420. The function _get_type has a single-line docstring with no sections, so D420 wouldn't fire on it. Could you point me to a specific D420 false positive you're seeing?

@o1x3 o1x3 requested a review from amyreese February 25, 2026 09:41
@ntBre
Copy link
Contributor

ntBre commented Feb 25, 2026

Regarding the linked langchain file (messages/utils.py:69): that line is a DOC201 change in the ecosystem diff, not D420. The function _get_type has a single-line docstring with no sections, so D420 wouldn't fire on it. Could you point me to a specific D420 false positive you're seeing?

I was going to say that we should make sure not to change other rules while adding D420, but these appear to be a hiccup in the ecosystem check since there are two pairs of +/- diagnostics adding and removing DOC201 diagnostics in exactly the same location. Thanks for checking!

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this is looking good! I just had a few minor suggestions beyond Amy's comments.

o1x3 and others added 2 commits February 25, 2026 21:52
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
@o1x3
Copy link
Contributor Author

o1x3 commented Feb 25, 2026

Thanks @ntBre

Should I squash all the commits once the review is done?

@amyreese
Copy link
Member

We'll squash when we merge the PR.

@ntBre
Copy link
Contributor

ntBre commented Feb 25, 2026

Yes, please don't squash or rebase after we've reviewed because it doesn't play well with GitHub's review interface! We always squash-merge as Amy said.

@amyreese amyreese changed the title [pydocstyle] Add rule to enforce docstring section ordering (D420) [pydocstyle] Add rule D420 to enforce docstring section ordering Feb 25, 2026
@amyreese amyreese merged commit 85dac63 into astral-sh:main Feb 25, 2026
42 checks passed
@o1x3 o1x3 deleted the pydocstyle/d420-section-order branch February 26, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docstring Related to docstring linting or formatting preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Request: Lint rule to enforce ordering of docstring sections

3 participants