latexfmt
latexfmt formats a LaTeX project to a set of house conventions. Point it at a
root .tex; it resolves every \input/\include and formats the body files.
As in TeX, an input path is relative to the root's directory wherever the
\input sits (the including file's directory is tried as a fallback). An input
that resolves to no file is reported, and label pruning is then turned off,
since a reference inside that file would be invisible.
A body file before and after latexfmt main.tex --columns 60:
The energy is bounded,
see \eqref{eq:bound}, which
follows from the convexity of the potential.
\begin{align}
E(x) &\leq C \|x\|^2 \label{eq:bound}
\end{align}
and the gradient satisfies
\begin{align}
\nabla E(x) &= A x \label{eq:grad} \\
\|\nabla E(x)\| &\leq L \|x\| \label{eq:lip}
\end{align}
\[
y = mx + b
\]
The energy is bounded, see \eqref{eq:bound}, which follows
from the convexity of the potential.
\begin{equation}\label{eq:bound}
E(x) \leq C \|x\|^2
\end{equation}
and the gradient satisfies
\begin{align}
\nabla E(x) &= A x \\
\|\nabla E(x)\| &\leq L \|x\|
\end{align}
\begin{equation*}
y = mx + b
\end{equation*}
The prose is reflowed; the single-row align becomes an equation, keeping its
referenced label; eq:grad and eq:lip are referenced nowhere in the project and
are pruned; \[ \] becomes equation*. The project is then rebuilt with latexmk
to check that no reference broke.
Installation
latexfmt requires Python 3.13 or newer. Install the command from PyPI into an
isolated uv tool environment:
uv tool install latexfmt
latexfmt --version
uv tool upgrade latexfmt updates it. To run an unreleased version, install from
the Git repository instead, for example
uv tool install --reinstall git+https://github.com/nbrosse/latexfmt.git@main. If uv reports that its executable
directory is missing from PATH, run uv tool update-shell once.
For development without installation, point uv run --project or uvx --from
at a local checkout.
Usage
# from the LaTeX project directory
latexfmt main.tex # format + verify with latexmk
latexfmt main.tex --dry-run # unified diff, change nothing
latexfmt main.tex --check # summary only, exit 1 if it would change
# development checkout alternatives
uv run --project /path/to/latexfmt latexfmt main.tex
uvx --from /path/to/latexfmt latexfmt main.tex
Exit codes
| code | meaning |
|---|---|
0 |
success |
1 |
files would be reformatted (--check / --dry-run only) |
2 |
usage or I/O error (missing root, undecodable file) |
3 |
latexmk build failed, or --strict and the verify pass found something |
--check follows the black/ruff convention, so latexfmt main.tex --check
works directly in CI or a pre-commit hook.
pre-commit
The repository ships a pre-commit hook. latexfmt
works on a whole project from its root file, so the hook receives that root as an
argument rather than the list of staged files:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/nbrosse/latexfmt
rev: v0.2.1
hooks:
- id: latexfmt
args: [main.tex, --check]
--check changes nothing and does not run latexmk; it fails the commit when a
file would be reformatted. Drop --check (and add --no-build to keep commits
fast) to have the hook format the files instead.
All transformations are prepared before the first write, individual writes
replace files atomically, and a strict-verification or latexmk failure restores
the original sources. TeX build artifacts are not rolled back.
What it does
In order, per file: normalize whitespace (tabs → spaces, strip trailing) · prune
unreferenced \labels inside display math (referenced-anywhere is
auto-detected across the whole project) · pick math environments (single-row
align → equation, keep multi-row align; autonum-aware,
numbering-preserving) · indent 2 spaces per environment level with proof
bodies flush-left · reflow prose to 100 columns (VSCode Rewrap-style) ·
standardize equation bodies (compact, wrapped at 100, AMS break-before-operator,
no orphan operators) · collapse blank lines. It then finalizes indentation with
latexindent and, unless --no-build, rebuilds with latexmk, failing on
undefined or multiply-defined references.
Opaque environments — tabular, tabularx, longtable, verbatim,
lstlisting, minted, tikzpicture, comment — are left byte-for-byte alone
by every pass, and are exempt from the verify checks. This is checked, not only
intended: if a pass would change an opaque body, the file is refused before
anything is written. Hand-aligned tables keep
their tabs.
Comments are preserved everywhere, including inside display math. With
--wrap-comments, only full-line comment paragraphs are reflowed and every
continuation line retains its % marker; trailing comments remain untouched. A
commented-out \label is never pruned nor promoted to a live one, and a \\,
& or \end{...} inside a comment never changes how a block is parsed.
Missing latexindent/latexmk are detected and skipped with a warning (the
built-in Python indentation is used as a fallback).
Flags
--columns · --indent · --encoding · --all (also format root + preamble) ·
--keep-labels · --wrap-comments · --no-latexindent · --no-build ·
--backup · --strict · --dry-run · --check · --version. CLI flags
override the config file, which overrides the built-in defaults. See
latexfmt.toml for every option.
Configuration
Installing the tool does not install a project configuration. Put a
latexfmt.toml in the LaTeX project, normally beside main.tex:
columns = 100
indent = 2
encoding = "utf-8"
prune_labels = true
wrap_comments = false
build = true
latexindent = true
latexindent_config = ".latexindent.yaml"
strict = false
all = false
backup = false
exclude = ["*_old*", "*sections_old*"]
The same keys can instead live under [tool.latexfmt] in a project's
pyproject.toml:
[tool.latexfmt]
columns = 100
indent = 2
build = true
Configuration precedence is: CLI flags, then the project configuration, then
built-in defaults. latexfmt discovers a configuration by walking upward from
the directory containing the root .tex; discovery does not start from the
shell's current directory or from the installed package. Pass
--config /path/to/config.toml to select a file explicitly. A missing explicit
file or an invalid value is reported as an error before source files are
processed.
latexindent_config names a separate YAML configuration consumed by
latexindent; a relative path is resolved from the root .tex directory. It
does not refer to latexfmt.toml itself.
If no .latexindent.yaml is found, latexfmt falls back to the copy bundled in
the installed package and says so in the summary. This matters because running
latexindent without that YAML would apply its own defaults—tab indentation,
& column re-padding, and no protected verbatim/tikz/tabular environments—which
would undo earlier formatting passes. The .latexindent.yaml in this repository
matches the bundled copy and can be copied into a project when customization is
needed.
Relation to latexindent and tex-fmt
latexfmt is not a competing formatter — it is a project-level, semantics-aware
pipeline that uses a line formatter as its final indentation step.
latexindent (Perl) is that backend
today; tex-fmt (Rust) is a faster,
Perl-free alternative in the same category. Both are pure line/whitespace
formatters: by their own scope they do no semantic parsing, so neither does the
work that motivates this package — selecting math environments (align →
equation, numbering-preserving, autonum-aware), standardizing equation bodies,
pruning unreferenced \labels across the project, resolving \input/\include,
or verifying the result with a latexmk build.
tex-fmt is not wired in as a backend for the moment. It could later slot into the
same finalizer seam as latexindent (run with --nowrap, since latexfmt already
reflows prose itself), but doing so trades latexindent's fine-grained convention
fidelity (see .latexindent.yaml: proof bodies flush-left, no & re-padding,
protected verbatim/tikz/tabular blocks) for speed and a lighter dependency. Until
that tradeoff is worth wiring up, latexfmt stays on latexindent.
Development
uv sync # install with dev dependencies
uv run pytest # 137 hermetic tests, plus 4 integration tests if TeX is installed
uv run ruff check .
The suite is golden-file based: tests/cases/<name>/ holds in.tex,
expected.tex and an optional opts.json. Every case is also asserted
idempotent — reformatting formatted output must be a no-op. After an
intentional change, regenerate with
LATEXFMT_REGOLD=1 uv run pytest tests/test_golden.py
and read the resulting diff before committing it.
tests/test_integration.py is the only part that shells out to the real
latexindent and latexmk; it skips itself when they are not on PATH.
License
Apache 2.0 — see LICENSE.
Metadata
Release files for latexfmt 0.2.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 | |
|---|---|---|---|
| latexfmt-0.2.1.tar.gz | 25.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| latexfmt-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.7 kB
Release files / latexfmt-0.2.1.tar.gz
| Download URL | latexfmt-0.2.1.tar.gz |
|---|---|
| Size | 25.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3d3ee56c6c5645cce718e0986476ef9064e7d9263eaafdba40fb2cea3507d0fa
|
|
BLAKE2b-256 checksum How to use checksums |
fe1c0f3b24479f0acc20233d7ba955715b1ccd41a942575910dd4a53da898c98
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / latexfmt-0.2.1-py3-none-any.whl
| Download URL | latexfmt-0.2.1-py3-none-any.whl |
|---|---|
| Size | 29.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b97660adfbf8812b53b5831f1bc14d0086b561b29d7dbcb9887a1f6995c4757c
|
|
BLAKE2b-256 checksum How to use checksums |
45ceb76e932252a53c1a5e116bc8c09661d9142003af074fbcf43cb0c1f5d1fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|