Markdown Linting
Documentation quality is maintained through automated markdown linting using pymarkdownlnt.
Why Separate from Code Linting?
Markdown linting is intentionally separated from Python code linting for several reasons:
- Different concerns - Documentation style vs code quality
- Different audiences - Documentation maintainers vs developers
- Different fix workflows - Content editing vs code refactoring
- Independent failures - Docs issues shouldn't block code deployment
- Clearer CI feedback - Easy to identify doc vs code problems
About pymarkdownlnt
pymarkdownlnt is the recommended modern markdown linter for Python projects:
✅ Pure Python - No Ruby/Node.js dependencies (unlike markdownlint) ✅ Configurable - Flexible rule configuration via pyproject.toml ✅ Fast - Efficient scanning of large documentation sets ✅ Active development - Regular updates and improvements ✅ CLI + Library - Can be used standalone or integrated
Alternatives Considered
| Tool | Pros | Cons | Verdict |
|---|---|---|---|
| pymarkdownlnt | Pure Python, fast, configurable | Younger project | ✅ Best for Python projects |
| markdownlint-cli | Popular, mature, extensive rules | Requires Node.js | ❌ External dependency |
| mdl (Ruby) | Well-established | Requires Ruby | ❌ External dependency |
| remark-lint | Plugin ecosystem | Requires Node.js, complex setup | ❌ Too complex |
Configuration
Markdown linting is configured in pyproject.toml:
[tool.pymarkdown]
plugins.md013.enabled = false # Disable line length (tables can be long)
plugins.md033.enabled = false # Allow inline HTML (badges, images)
plugins.md036.enabled = false # Allow emphasis as pseudo-headings
extensions.front-matter.enabled = true # YAML front matter support
extensions.tables.enabled = true # Enable tables
Key Rules
| Rule | Description | Status | Reason |
|---|---|---|---|
| MD013 | Line length | ❌ Disabled | Tables, code blocks, long URLs |
| MD033 | Inline HTML | ❌ Disabled | Badges, centered images, styling |
| MD036 | Emphasis as heading | ❌ Disabled | Common in step lists, examples |
| MD031 | Blank lines around code | ✅ Enabled | Improves readability |
| MD040 | Code fence language | ✅ Enabled | Enables syntax highlighting |
Running Locally
Scan All Documentation
# Scan all markdown files
uv run pymarkdown --config pyproject.toml scan docs/ README.md
# Scan specific directory
uv run pymarkdown --config pyproject.toml scan docs/development/
# Scan single file
uv run pymarkdown --config pyproject.toml scan README.md
Fix Issues
pymarkdownlnt doesn't auto-fix issues. Fixes must be manual:
- Run scan to identify issues
- Review error messages
- Edit files to resolve issues
- Re-run scan to verify
Common fixes:
Example: MD031 - Add blank lines around code blocks
Before - text immediately adjacent to code:
After - blank lines around code blocks:
More text
After - language specified:
CI/CD Integration
Markdown linting runs as a separate CI stage from code linting.
Pipeline Position
This allows:
- ✅ Code linting to pass independently
- ✅ Documentation fixes without blocking deployments
- ✅ Clear separation of concerns
CI Configuration
File: .github/workflows/markdown-lint.yml
- name: Run markdown linting
run: |
uv run pymarkdown scan docs/ README.md
continue-on-error: true # Won't block pipeline
Failure Behavior
- Status:
continue-on-error: true(warnings only) - When: Runs on pushes/PRs with doc changes
- Impact: Non-blocking - won't prevent merges
- Visibility: Shows warning in GitHub Actions
Best Practices
When Writing Docs
- Use proper heading hierarchy - Don't skip levels (H1 → H2 → H3)
- Add language to code blocks - Enables syntax highlighting
- Blank lines around blocks - Code, lists, quotes need spacing
- Consistent list markers - Use
-for unordered,1.for ordered - No trailing whitespace - Clean up line endings
Common Pitfalls
❌ Avoid:
# Heading
## Subheading
#### Sub-sub-heading # Skipped H3!
code without blank line above (missing blank line and language)
List without spacing:
- Item 1
- Item 2
Next paragraph (no blank line before)
✅ Prefer:
List with proper spacing:
- Item 1
- Item 2
Next paragraph with blank line.
### Long Lines
Since MD013 is disabled, be reasonable:
- ✅ **OK**: URLs, table content, code blocks
- ✅ **OK**: Short overruns (85-100 chars)
- ❌ **Avoid**: Very long prose paragraphs (>120 chars)
Break long paragraphs naturally:
**Bad:**
```text
This is a really long paragraph that goes on and on...
Good:
This is a paragraph that is broken into reasonable line lengths.
It improves readability and makes diffs clearer.
Troubleshooting
No Output from Scan
If pymarkdown runs but shows no output:
# Verify installation
uv run pymarkdown --version
# Check config
uv run pymarkdown plugins list
# Try with verbose output
uv run pymarkdown scan docs/ --verbose
Too Many Errors
If overwhelming number of errors:
- Fix incrementally - Start with one directory
- Disable strict rules - Update
pyproject.toml - Focus on critical - MD040, MD031 are most important
- Batch similar fixes - Fix all MD040s at once
False Positives
If a rule incorrectly flags valid markdown:
- Check if it's actually valid - Validate markdown spec
- Disable the rule - Add to
pyproject.tomlignore list - Use inline ignore -
<!-- markdownlint-disable MD### -->
Integration with Editors
VS Code
Install extension: Markdown Lint (davidanson.vscode-markdownlint)
Settings (.vscode/settings.json):
Pre-commit Hook (Optional)
Add to .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: pymarkdown
name: Markdown Linting
entry: uv run pymarkdown scan
language: system
files: \\.md$
pass_filenames: true
Maintenance
Updating Rules
To enable/disable rules:
- Edit
pyproject.tomlunder[tool.pymarkdown] - Test locally:
uv run pymarkdown scan docs/ - Commit and push
- Verify in CI pipeline
Batch Fixes
When adding new rules or fixing legacy docs:
# Scan and save results
uv run pymarkdown scan docs/ > markdown-issues.txt
# Fix by category
# 1. Fix all MD040 (code block languages)
# 2. Fix all MD031 (blank lines)
# 3. Fix remaining issues
# Verify
uv run pymarkdown scan docs/
Resources
Summary
Key Takeaways:
✅ pymarkdownlnt is the best pure-Python markdown linter ✅ Separate markdown linting from code linting in CI ✅ Configure sensibly - disable overly strict rules ✅ Non-blocking in CI - won't prevent deployments ✅ Manual fixes required - no auto-format available ✅ Focus on readability and correctness, not perfection