simplemark
A small markup language for Python docstrings and argparse help text.
It is a subset of CommonMark's constructs, with one narrowed inline rule: emphasis delimiters must be flanked by whitespace or punctuation, so emphasis never happens inside a word. A docstring with no diagnostics renders identically in simplemark and under a Markdown renderer.
It exists to answer a question asked and left unanswered in a python.org discussion about rich text in pydoc and argparse: if just a subset will do, what markdown features can be safely omitted, and does omitting them actually reduce the complexity?
The measurement
Every docstring in the Python 3.12.13 standard library, all 7272 of them, run through the checker:
| docstrings | share | |
|---|---|---|
| clean: no diagnostics, unchanged | 6762 | 93.0% |
| containing an error | 260 | 3.6% |
What the corpus says a docstring format can drop:
| feature | docstrings using it | verdict |
|---|---|---|
# headings |
7 | kept anyway, on familiarity grounds: the one taste call |
| reST underline headings | 21 | omit |
| numbered lists | 9 | keep, it costs nothing |
*emphasis* |
251 | keep, mostly marking parameter names |
**strong** |
1 | keep, but the debate about it is misplaced |
| verbatim blocks | 4.2% | the core convention; cannot be dropped |
And what CommonMark gets wrong about Python prose, which the narrowed rule fixes:
x = xc*10**xe and y = yc*10**ye, compute x**y. CommonMark: <em>/<strong>. simplemark: literal.
The __init__ and __del__ methods. CommonMark: <strong>. simplemark: literal.
That rule is why *args, **kwargs, f(*args, **kwargs), 10**e, __init__, _private, a_b_c, *.py, C:\dir, \d+ and [start, [stop]] are all literal text, with no escaping.
The language
Five block types and five inline forms. The whole grammar:
# Overview
Compute `n` factorial, in **bold** and *italic*.
Example:
>>> fact(5)
120
- a bullet
- another
1. numbered
2. list
See [the docs](https://example.com).
No definition lists, no tables, no block quotes, no images, no footnotes, no raw HTML, no nested inline markup.
reStructuredText constructs (::, :role:, .. directive, legacy `quoted' text) are not part of the language. check(compat=True) finds them, so an existing codebase can migrate deliberately.
Use
import simplemark
doc = simplemark.parse(docstring)
print(simplemark.render_text(doc, width=72))
for diagnostic in simplemark.check(docstring):
print(diagnostic) # 12:5: CM001 CommonMark reads some of these ...
for diagnostic in simplemark.check(docstring, compat=True):
print(diagnostic) # ... plus L001 trailing '::' is reStructuredText
parse, parse_inline, check and render_text never raise, on any input.
python -m simplemark --width 72 file.txt
python -m simplemark --check file.txt
python -m simplemark --check --compat --strict file.txt
See the before/after that motivates it, and the evidence behind it:
uv run python tools/pydoc_demo.py os.walk --width 60
uv run python tools/corpus_report.py
Sphinx
A project that adopts simplemark and builds its docs with autodoc needs its docstrings read as reStructuredText, or [a](link) and # Heading arrive as literal punctuation. tools/sm2rst.py renders the same AST as reST in 140 lines, with no changes to the parser:
printf '# Title\n\nUses `n` and 10**e.\n' | uv run python tools/sm2rst.py
It is not part of the package: pydoc and argparse render to terminals and have no use for reST, so shipping it would add a sixth of the size for nothing the core case needs.
Size
| module | lines of code |
|---|---|
_parser.py |
394 |
_render.py |
220 |
_nodes.py |
75 |
__init__.py, _cli.py, __main__.py |
99 |
| total | 788 |
No runtime dependencies. Linear time, no backtracking, two small regular expressions in the block grammar.
A bug found along the way
inspect.cleandoc takes the smallest indentation in the body as the margin. When a code block is the only indented content, the block's own indentation becomes the margin and is stripped, flattening the example:
def f():
"""Example::
>>> f()
"""
# inspect.cleandoc gives 'Example::\n\n>>> f()' -- the block is gone
Simplemark's margin differs from cleandoc's in 179 standard library docstrings (3.9% of the multi-line ones); in 65 of them cleandoc destroys a verbatim block outright. simplemark takes the margin from the closing-quote line when the docstring ends with one, which is exactly the enclosing indentation; 50% of stdlib docstrings provide that hint.
Design notes
The reasoning, the alternatives that were rejected, and why, are in notes/:
| 00-prior-art.md | Every previous attempt, and why each died |
| 01-vision.md | The problem, goals, non-goals, failure modes |
| 02-use-cases.md | Corpus data and 24 use cases, as acceptance criteria |
| 03-design-alternatives.md | Every decision, its options, and its status |
| 04-specs.md | The specification |
| 05-implementation.md | How the code is built, and what it deliberately is not |
Tests
uv run pytest
275 tests. tests/conformance.json is the behavioural specification as data, so a second implementation is possible. tests/b_integration/test_differential.py checks the normative claims against the real markdown-it-py rather than asserting them. The standard library is the fuzz corpus.
Status
Design and reference implementation, intended as a contribution to the discussion rather than as a competing library. Published on PyPI as simplemark; source at https://git.sr.ht/~sfermigier/simplemark; MIT licensed.
Metadata
Release files for simplemark 0.1.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 | |
|---|---|---|---|
| simplemark-0.1.1.tar.gz | 15.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| simplemark-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.0 kB
Release files / simplemark-0.1.1.tar.gz
| Download URL | simplemark-0.1.1.tar.gz |
|---|---|
| Size | 15.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b42d51f47910ce2892e2d25cfa0fdd27e04ba6480e30f3073638039f446d9c09
|
|
BLAKE2b-256 checksum How to use checksums |
f53f86c0ca013f5ffd3733d2ff4c13a3b82e782f89c5531bef30b317aee69ecc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / simplemark-0.1.1-py3-none-any.whl
| Download URL | simplemark-0.1.1-py3-none-any.whl |
|---|---|
| Size | 17.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
50d72273c41e3aae8ffc25a86b222a80d14428a58f5bd940bae0a893374fc5bf
|
|
BLAKE2b-256 checksum How to use checksums |
81d039e6ddbec309ba6d14936edbdbfef911cb03b45d28a79cfbb41c1a612d17
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|