Skip to content

Add built-in validation check for bullet lists missing a preceding blank line #686

@drbenvincent

Description

@drbenvincent

Issue

Sphinx with the Napoleon extension (which numpydoc-validation users almost universally pair with) silently mis-renders bullet lists inside docstring section bodies when the list is not separated from its introductory line by a blank line. The bullet markers get inlined into the introductory paragraph as literal characters rather than being parsed as a list.

Concrete example, taken from a real public-API method:

def analyze_persistence(self, ...) -> dict[str, Any]:
    """...

    Returns
    -------
    dict[str, Any]
        Dictionary containing:
        - "mean_effect_during": Mean effect during intervention period
        - "mean_effect_post": Mean effect during post-intervention period
        - "persistence_ratio": ...
    """

Napoleon collapses the four lines after Dictionary containing: into a single paragraph, so the rendered HTML reads as a wall of prose with literal hyphens rather than as four <li> elements. The fix is mechanically simple — insert a blank line between the introductory line and the first bullet — but the defect is invisible at edit time because the source is also valid (if uninspiring) RST.

This bit a downstream library recently (pymc-labs/CausalPy#892, where ~30 sites were affected across the public API).

Why existing tooling does not catch this

  • numpydoc-validation checks (GL01GL10, SS01SS06, PR01PR10, RT01RT05, SA01SA04, EX01, YD01, ES01) all validate section structure — section names, ordering, parameter sync, summary capitalisation, etc. None inspect the body content of a section.
  • Ruff's pydocstyle (D) rules cover docstring presence, surrounding whitespace, and blank lines around section headers (D406D416), but not within section bodies.
  • Sphinx itself does not warn — Napoleon happily produces the broken paragraph.
  • Markdown linters (pymarkdown, markdownlint) only operate on .md files, so they never see these docstrings.

Feature request

Add a new validation check, e.g.:

  • GL11 (or whatever code is free): "Bullet list inside docstring is not preceded by a blank line and will be rendered as inline prose by Sphinx Napoleon."

The detection rule that worked cleanly in practice (zero false positives across a 13k-line scientific Python codebase) is:

Flag a line that starts (after whitespace) with a bullet marker (-, *, or + followed by whitespace) when the immediately preceding non-blank line ends in : and is not itself a bullet line.

This is intentionally narrower than "all bullet lists must be surrounded by blank lines" — it only targets the unambiguous Napoleon-collapse pattern (intro: followed directly by bullets), which is the form that produces the silent rendering bug. Stricter forms can be added later as separate codes if there is appetite.

Precedent

Issue #507 (Add built-in validation checks for directive syntax) makes a structurally identical case: Sphinx silently fails or mis-renders, and numpydoc-validation is the natural place to lint for it before the docs build. This proposal is a sibling in the same spirit but for bullet-list shape rather than directive shape.

Reference implementation

A working AST-based detector (~30 lines) that we are using locally as a stop-gap:
https://github.com/pymc-labs/CausalPy/blob/main/scripts/validate_docstring_lists.py

It walks ast.walk over each module, calls ast.get_docstring(..., clean=False), and applies the rule above. Plumbing it into numpydoc's existing validation pipeline should be straightforward since the docstring is already extracted there.

Offer

Happy to send a PR if the maintainers think this is in scope — would prefer to confirm appetite before investing in the full submission (tests, docs, error-code allocation).

Thank you for your time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions