Skip to content

Diagnostics

Simplemark never refuses to render. Everything below is reported by check or simplemark --check, and nothing stops output being produced.

There are three severities, and the split is the whole point:

Severity Meaning Reported by default Fails --check
error Simplemark itself is ambiguous, or the author almost certainly erred yes yes
commonmark A Markdown renderer would read this differently yes no
legacy reStructuredText, or an older docstring convention no: --compat only no

Legacy notes are migration advice, not problems. A codebase that has never seen reStructuredText should never hear about them; a codebase migrating away from it wants every one. That is what the two modes are for, and they are the same parser with a filter on the output.

--strict promotes every reported diagnostic to a failure. It does not change what is reported.


Errors

Always reported.

E001: unclosed code span

A backtick run with no matching close.

python - <<'EOF'
import simplemark
for d in simplemark.check("A command `foo is dispatched."):
    print(d)
EOF
1:11: E001 unclosed code span

Fix: close it, or escape the backtick as \`.

E002: doctest is not indented

A paragraph beginning with >>> at column 0. It would be reflowed as prose, destroying the example.

python - <<'EOF'
import simplemark
for d in simplemark.check(">>> fact(5)\n120\n"):
    print(d)
EOF
1:1: E002 doctest is not indented

Fix: indent by four. 206 standard library docstrings have this: mostly module docstrings whose doctests sit flush left, or are indented by only two.


CommonMark notes

Always reported. These say: a Markdown renderer would disagree with simplemark here. If your docstrings are read on GitHub or in an IDE hover, they matter.

CM001: delimiters CommonMark reads as emphasis

The important one, and the reason for simplemark's single narrowed inline rule.

python - <<'EOF'
import simplemark
for d in simplemark.check("x = xc*10**xe and y = yc*10**ye, compute x**y."):
    print(d)
EOF
1:1: CM001 CommonMark reads some of these delimiters as emphasis

Simplemark leaves the arithmetic alone, because emphasis cannot start inside a word. CommonMark pairs the ** runs and puts <strong> through the middle of the formula. Same for __init__ and __del__ in one paragraph.

Fix: nothing is needed for simplemark. To render correctly on GitHub too, wrap the expression in a code span: `x**y`.

This note is deliberately conservative: it fires when the text could be mangled, without embedding a CommonMark parser to know for certain. _pydecimal.py produces a lot of them, and that is correct: those docstrings already render wrong on GitHub today.

CM002: a marker that would interrupt the paragraph

python - <<'EOF'
import simplemark
text = "My favourite number is probably the number\n1. It is the smallest natural number."
for d in simplemark.check(text):
    print(d)
EOF
2:1: CM002 CommonMark lets this marker interrupt the paragraph

Simplemark keeps it as one paragraph, because no block may interrupt a paragraph. CommonMark starts an ordered list.

Fix: reword, or escape the marker: 1\. It is….


Legacy notes

Reported only with --compat. These say: this looks like reStructuredText, or a convention that predates the format. Run them once when adopting simplemark on an existing codebase; ignore them forever after.

python - <<'EOF'
import simplemark
legacy = "Example::\n\nSee :mod:`typing` and\n\n.. note:: careful\n"
print("default:", simplemark.check(legacy))
for d in simplemark.check(legacy, compat=True):
    print("compat: ", d)
EOF
default: []
compat:  1:8: L001 trailing '::' is reStructuredText; write ':'
compat:  3:5: L002 ':role:' is reStructuredText
compat:  5:1: L003 '..' directive or comment is reStructuredText

L001: trailing ::

In reStructuredText, :: at the end of a paragraph introduces a literal block and renders as a single colon. Simplemark does not need it (a blank line and four spaces is enough), so both colons are shown.

Fix: write Example:. 64 standard library docstrings have this.

L002: :role:

:mod:`typing`, :class:`Foo` and friends. Usually copied from module documentation without cleanup; this is one of the two problems that started this project.

Fix: `typing`. 32 standard library docstrings.

L003: .. directive or comment

.. note::, .. versionadded::, and reST comments.

Fix: ordinary prose, or a bullet list. 4 standard library docstrings.

L004: definition-list shape

A short line followed by an indented body. reStructuredText reads it as a definition list; simplemark reads one paragraph.

python - <<'EOF'
import simplemark
for d in simplemark.check("module_relative\n  If true, paths are module-relative.\n", compat=True):
    print(d)
EOF
1:1: L004 reStructuredText reads this as a definition list

The most-reported note, 386 docstrings, and most are false alarms in spirit: Args: headers and hand-aligned prose that merely look like definition lists. See why the construct was left out.

Fix: if you meant a list, use bullets. If you did not, ignore it.

L005: legacy `quoted' text

A code span whose content looks like the old GNU quoting style, `foo'.

python - <<'EOF'
import simplemark
for d in simplemark.check("A command `foo' is dispatched to `do_foo'.", compat=True):
    print(d)
EOF
1:11: L005 code span looks like legacy `quoted' text

This one is worth taking seriously: with an even number of such quotes the backticks pair with each other, swallowing the prose between them into a single code span. The output is silently wrong. See the worked example.

Fix: `foo`. 28 standard library docstrings.


What the corpus produces

Every docstring in the Python 3.12 standard library, 7272 of them:

docstrings share
clean in default mode 6762 93.0%
containing an error 260 3.6%
Code Severity docstrings share
L004 legacy 386 5.3%
CM001 commonmark 219 3.0%
E002 error 206 2.8%
L001 legacy 64 0.9%
E001 error 54 0.7%
CM002 commonmark 48 0.7%
L002 legacy 32 0.4%
L005 legacy 28 0.4%
L003 legacy 4 0.1%

Regenerate with uv run python tools/corpus_report.py.