lastlint: LaTeX strict linter
A linter for Evan Chen's LaTeX style guide.
Usage
Pre-commit hook
Add to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/cjquines/lastlint
rev: v0.6.0 # x-release-please-version
hooks:
- id: lastlint-fix
To only report errors rather than auto-fixing them,
use the lastlint hook id.
Standalone
Install from PyPI. Using uv (recommended):
uv tool install lastlint
lastlint path/to/file.tex another.tex
To run without installing: uvx lastlint path/to/file.tex.
With pip: pip install lastlint.
Multiple files are accepted, and glob patterns are expanded.
Pass --fix to auto-fix supported rules in place.
Pass --ignore E001,E013 to skip both reporting and fixing.
On a terminal, output is grouped per file and colorized;
when piped or redirected it uses a plain
file:line:col: EXXX: message
format parseable by editors.
Force either mode with --color always or --color never
(also honors NO_COLOR and FORCE_COLOR).
Editor
An optional language server exposes lastlint to compatible text editors.
Install it with the lsp extra:
uv tool install 'lastlint[lsp]'
# or: pip install 'lastlint[lsp]'
The server lastlint-lsp, over stdio,
publishes diagnostics and implements formatting,
so format-on-save will auto-fix LaTeX files.
Pass ignored rules through initializationOptions,
e.g. {"ignore": ["E001", "E013"]}.
The setups below assume lastlint-lsp is globally installed.
To skip a global install, replace the command with
uvx --from 'lastlint[lsp]' lastlint-lsp.
Neovim
0.11+, native vim.lsp:
vim.lsp.config('lastlint', {
cmd = { 'lastlint-lsp' },
filetypes = { 'tex', 'plaintex' },
root_markers = { '.git' },
})
vim.lsp.enable('lastlint')
Sublime Text
The LSP package, in LSP.sublime-settings:
{
"clients": {
"lastlint": {
"enabled": true,
"command": ["lastlint-lsp"],
"selector": "text.tex.latex"
}
}
}
Emacs
Eglot (built
in since 29) — then run M-x eglot in a TeX buffer:
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'((latex-mode LaTeX-mode) . ("lastlint-lsp"))))
Suppression
Add an inline comment to skip rules on a single line:
This text has "intentional" quotes. % lastlint: disable=E002
Rules
Fixable rules are marked with 🔧.
| ID | Rule from style guide |
|---|---|
| E001 | Line length must be ≤ 120 |
| E002 🔧 | No literal " |
| E003 🔧 | Operators (\sin etc.) must be commands |
| E004 🔧 | No \ldots, \cdots, or ... |
| E005 🔧 | No grammatical punctuation inside inline math |
| E006 🔧 | No extraneous space before punctuation |
| E007 | Spacing around = must be symmetric |
| E009 🔧 | No bad math operators (| etc.) |
| E010 🔧 | No $$ ... $$ |
| E011 | No adjacent \[ ... \] blocks |
| E012 | \begin{align*} / \end{align*} must have own line |
| E013 🔧 | Env contents must be indented |
| E014 🔧 | No trailing whitespace |
| E015 | No \\\\ paragraph break |
| E017 🔧 | No : for function signatures |
Rules not implemented:
- Rule 8 (balanced delimiters): hard to lint reliably.
- Rule 16 (variables in prose): requires natural-language understanding.
E001
E001 skips two kinds of unfixable long lines:
those containing a URL (http:// or https://),
and fully-commented lines (only whitespace before the %).
Lines with a long trailing comment after real content are still flagged.
I recommend SemBr for automatically adding line breaks.
E004
E004 picks the right \dots variant from context
(\dotsc after a comma, \dotsb after an operator)
and leaves a \cdots unresolved when neither side determines spacing.
E013
E013 checks every env except a small denylist:
document, center, quote,
and the verbatim envs (asy, tikzpicture, verbatim, lstlisting, …).
See NO_INDENT_ENVS in lastlint.py to adjust.
The --fix pads under-indented lines inside indent envs to 2 * depth spaces
(already-correctly-indented or deeper lines are left alone),
and converts leading tabs to 2 spaces each.
Development
pip install -e .[dev]
pytest
Metadata
Release files for lastlint 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lastlint-0.6.0.tar.gz | 20.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lastlint-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 38.2 kB
Release files / lastlint-0.6.0.tar.gz
| Download URL | lastlint-0.6.0.tar.gz |
|---|---|
| Size | 20.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d43220dac2af36a998a38bf8c1f4d6e65f9902a21411072af2fa3352910ba5a4
|
|
BLAKE2b-256 checksum How to use checksums |
77e1a926c3d1d3e6f55ec0b4e032b33ec4468966c84e68aad67be51b64f73da6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logRelease files / lastlint-0.6.0-py3-none-any.whl
| Download URL | lastlint-0.6.0-py3-none-any.whl |
|---|---|
| Size | 17.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dd554f2a0edab4ae3407972fda686c962a2ab6f2ff4db71d3f0b26eb60e6779d
|
|
BLAKE2b-256 checksum How to use checksums |
27a4e466013eb8ed51dc5407f42b9641fc1d1b90157efd5b08b6700a737f9849
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency log