A markdown style linter for technical writing - flags bold/italic, em dashes, banned phrases, long sentences, label-colon openers, and more.
Project description
stylint
Voice, formatting, and code-style rules for technical write-ups, plus a mechanical checker that catches the worst offenders.
Install
uv tool install stylint
Or from a local clone:
git clone https://github.com/alexeygrigorev/stylint ~/git/stylint
uv tool install --from ~/git/stylint stylint
Then run:
stylint docs/
stylint --list-tags
stylint --ignore tables docs/
stylint --style-guide
What's here
style-guide/voice.md- tone, voice rules, content rules (include/leave out).style-guide/formatting.md- markdown mechanics: headings, code blocks, lists, links, asides.style-guide/code-style.md- educational code style inside example blocks (no defensivetry/except, no chained.get(), one blank line between definitions).style-guide/polish.md- judgment-level prose patterns the script cannot detect (plain words over abstractions, banned-but-context-sensitive words).- The same style guide docs are bundled in the Python package. Run
stylint --style-guideto print their installed paths. stylint/- mechanical checker package. EditBANNED_WORDS,BANNED_PHRASES,BANNED_OPENERSinstylint/patterns.pyto extend the enforced list.check_style.py- compatibility wrapper for direct script usage.
Run the checker
Scan the current directory:
stylint
Scan one file or a specific folder:
stylint path/to/post.md
stylint docs/
Suppress specific rules with --ignore:
stylint --ignore tables docs/
stylint --ignore tables,long-clause-likely docs/
stylint --list-tags
To skip files, drop a .prose-style-ignore at the scan root, one
fnmatch pattern per line:
# build outputs
_site/*
node_modules/*
# generated docs
docs/api/*
Rule tags
Every finding starts with a [tag] so you know which rule fired and
can pass that tag to --ignore.
Markdown mechanics
bold-**text**or__text__used for emphasis.italic-*text*or_text_used for emphasis.tables- GFM table row (a line starting with|).hr- horizontal rule---on its own line.em-dash- the em-dash character—.double-hyphen---used as a substitute for an em dash.dash-parenthetical- sentence with two-dashes inserting a parenthetical.smart-quotes- curly quotes‘,’,“,”.backticks-in-link- link text wrapped in backticks like[`file.py`](url).bare-url- raw URL in prose likesee https://example.com.angle-url- URL wrapped in angle brackets like<https://...>.frontmatter-blank- missing blank line between YAML frontmatter closing---and the body.
Headings
heading-question-word- heading starts withWhy,How,What, etc.heading-question-mark- heading ends with?.heading-too-deep- heading depth###or deeper.lazy-heading- heading like## The problem,## The RAG idea,## The challenge.
Code blocks
code-no-lang- fenced code block with no language tag.code-too-long- code block over 40 lines.consecutive-code- two code blocks back-to-back with no prose between them.lead-in- code block or list directly under a heading with no lead-in sentence.lead-in-multi- the paragraph that introduces a list or code block has multiple sentences and ends with:. The lead-in sentence should be its own one-sentence paragraph.chained-get- chained.get(...).get(...)in example Python.double-blank- two blank lines between Python definitions.
Banned tokens (extend in stylint/patterns.py)
banned-word- single word on the BANNED_WORDS list (e.g.delve,leverage,crucial).banned-phrase- multi-word phrase on the BANNED_PHRASES list (e.g.the power of,serves as,in action,the backbone of).banned-opener- sentence-start word on the BANNED_OPENERS list (e.g.Additionally,Moreover).
Voice / fragments
third-person- the stringAlexeyappears in prose (third-person presenter reference).anaphoric-no- sentence-startNo X, no Yverbless fragment (e.g.No frameworks, no magic).semicolon- any;in prose.cleft- pointless[This/That/It] is what X is aboutcleft.gerund-opener- sentence opens with an-ingparticipial phrase (e.g.Reading through it, I noticed...).
Paragraph / sentence shape
paragraph-too-long- paragraph with more than 5 sentences.long-sentence- sentence over 20 words, no commas.long-list-likely- sentence over 20 words with commas where a colon precedes the commas or the sentence closes withand X/or Xover 3+ chunks. The commas look like enumeration. Fix: convert to a bullet list.long-clause-likely- sentence over 20 words with commas where the commas look like clause boundaries (subordinating conjunctions likewhich,that,while,because,but,however,so,when, or chains of actions with mixed subjects). Fix: split into 2-3 shorter sentences. Do NOT bullet — it would break the meaning.many-commas- sentence with more than 3 commas.colon-inline- colon followed by 3+ comma-separated items and a terminaland/or(likely should be a bullet list).parallel-sentences- 3+ adjacent sentences sharing the same 1-2 word opener (e.g.Maybe it should... Maybe it should...).choppy-rhythm- 3+ adjacent sentences of 9 words or fewer. The staccato rhythm reads worse than one or two joined sentences. Fix: combine two of them with a conjunction ("so", "because", "and", "but") or restructure as a single longer sentence.repeated-and- polysyndetic chain ("A and B and C") of 3+ items joined byandinstead of the oxford comma form. Fix: use commas withandonly between the last two items. Idioms like "more and more" or "date and time" only fire when embedded in a longer chain ("X and more and more Y"); standalone uses don't trigger the rule.meta-framing- sentence has the shape[The/A/Another] <abstract noun> [of X] is that <claim>(e.g.A big advantage of Recorder is that it keeps recording,Another limitation is that...,The key insight is that...). Writer announces the shape of the claim instead of stating it. Fix: drop the framing and lead with the claim.label-colon- paragraph opens withThe problem:,Goal:,What we want:followed by prose.Note:andImportant:are exempt as callouts.question-opener- paragraph opens with a rhetorical question (Why do we need X?).
File-level
now-lets-overuse- file uses more than 3 sentence-startingNow/Let'sopeners total. Vary openers or use the bare imperative.now-lets-combo- sentence starts with the redundant pairNow let's...orLet's now.... Drop both softeners and use the bare imperative.
Reading order for a write-up
Run stylint --style-guide to locate the installed copies when you
consume Stylint as a dependency.
voice.mdwhile drafting.formatting.mdfor any markdown syntax question.code-style.mdfor example code blocks.- Run
stylintand fix every reported finding. polish.mdfor a final judgment-level pass.
Hooking it into a project
The simplest setup is a pre-commit hook. From the target project root:
ln -s ~/git/stylint/check_style.py .git/hooks/post-add-style-check
Or run it from CI:
- run: stylint docs/
Notes on what's enforced vs. what's judgment
The script catches every rule listed in the "Rule tags" section above. Each error message includes the tag, a numbered list of common fixes, and (for list-conversion rules) the inline heuristic for deciding whether a bullet list is right.
Everything else (tone, voice, abstractions, "explain why", source material handling) is in the reference docs and needs a human or agent to apply judgment.
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 stylint-0.1.0.tar.gz.
File metadata
- Download URL: stylint-0.1.0.tar.gz
- Upload date:
- Size: 49.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.14.3 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
56cbcecc5fe78f48eb18836d964ae30c5d686cc7e480813e53cfae7353c7741a
|
|
| MD5 |
d8bf62b448d8fae3e2fb9e94234c1136
|
|
| BLAKE2b-256 |
f40d6b8791dccf901d49c50389091350d090bd7820a4e7873eabae265325b4b0
|
File details
Details for the file stylint-0.1.0-py3-none-any.whl.
File metadata
- Download URL: stylint-0.1.0-py3-none-any.whl
- Upload date:
- Size: 47.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.16.5 cpython/3.14.3 HTTPX/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
429ab0332e6a4224942b9a7c422e73c394d090041c52330b5a4186e47d2f76cc
|
|
| MD5 |
a062463f9494fac283d05f26b7b88b10
|
|
| BLAKE2b-256 |
3f981ad1ba2599b6f694215691249dbf9f476439c2eddb2e6f91a85e763ea4c7
|