Linting and formatting
Linting and formatting
Linters and formatters look similar but play different roles. This article covers the definition of each, the leading tools, and the trade-offs.
1. About linters and formatters
A linter statically inspects code for meaning, style, and potential defects. Diagnostics like "this variable is declared but never used" or "this function calls async without await". The name traces back to lint, a C analyzer Stephen Johnson built at Bell Labs in 1978.
A formatter rewrites code style (indentation, line breaks, whitespace) to be consistent. It does not change meaning. Prettier's official page calls itself "an opinionated code formatter" and intentionally minimizes options. Fewer options is a design decision that lowers consensus cost.
The two roles can be split, or a single tool can do both.
2. JS / TS
| Tool | First release | Role |
|---|---|---|
| ESLint | 2013 | Started by Nicholas C. Zakas. AST-based linter. Plugin ecosystem. |
| Prettier | 2017 | James Long. Opinionated formatter. |
| TypeScript ESLint | 2019 | typescript-eslint.io. Runs ESLint rules on the TS AST. |
| Rome → Biome | 2023 | After the Rome project shut down, the community fork Biome launched. Rust-based. Combines formatter and linter. |
| dprint | 2020 | Rust-based formatter. Multi-language plugins. |
| Oxlint / oxc | 2023 | Rust-based ESLint-compatible linter. Speed-focused. |
Two common combos:
- ESLint + Prettier — The biggest ecosystem. Rule conflicts are turned off via
eslint-config-prettier. ESLint's plugin breadth (React Hooks, Next.js, Tailwind, security, etc.) is the strength. - Biome alone — Bundles formatter and linter into one binary, fast because it's Rust. Biome's official page reports about 97% Prettier compatibility and roughly 35x speed. It hasn't ported every ESLint rule (currently around 491), so check before switching if specific plugins are critical.
3. Python
| Tool | First release | Role |
|---|---|---|
| pylint | 2003 | The oldest Python linter. Wide checks, on the slower side. |
| flake8 | 2010 | Wraps pycodestyle + pyflakes + mccabe. |
| isort | 2013 | Timothy Crosley. Specialist import sorter. |
| Black | 2018 | A PSF project. Opinionated formatter (almost no options). |
| Ruff | 2022 | Astral. Rust. Absorbs many flake8 / isort / pydocstyle / pyupgrade / autoflake rules into one tool. Later added Ruff Format (2023) for Black-compatible formatting. |
The Ruff docs cite "10–100x faster". A single binary, pyproject.toml config, and auto-fix (--fix) accelerated adoption.
4. Java
| Tool | Released | Role |
|---|---|---|
| Checkstyle | 2001 | Style and convention checks. The oldest. |
| PMD | 2002 | Static analysis. Code smells, complexity. |
| SpotBugs (FindBugs successor) | 2017 | Bytecode-based latent bug detection. |
| google-java-format | 2015 | Formatter for the Google style guide. |
| Spotless | 2016 | Gradle / Maven plugin. Integrates formatters into the build phase. |
Spotless is often picked for build integration. A single ./gradlew spotlessApply formats everything.
5. pre-commit hook integration
- lint-staged (Node) + Husky — Runs ESLint / Prettier only on staged JS / TS files. Quick on small PRs.
- pre-commit (pre-commit.com) — Multi-language. List tools like Ruff, Black, Checkstyle in
.pre-commit-config.yaml. - lefthook (Go) — Single binary, language-agnostic.
6. Common shapes
ESLint (flat config, v9+):
// eslint.config.mjs
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{ rules: { "no-console": "warn" } },
];
Prettier:
// .prettierrc
{ "semi": true, "singleQuote": true, "trailingComma": "all", "printWidth": 100 }
Biome:
// biome.json
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
"linter": { "enabled": true, "rules": { "recommended": true } }
}
Ruff (pyproject.toml):
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
ignore = []
Commands:
# Common
pnpm exec eslint .
pnpm exec prettier --write .
pnpm exec biome check --write .
# Python
uv run ruff check .
uv run ruff format .
# Java
./gradlew spotlessCheck
./gradlew spotlessApply
7. Trade-offs
Biome vs ESLint + Prettier:
| Axis | ESLint + Prettier | Biome |
|---|---|---|
| Config simplicity | Two tools each + conflict-avoidance config | One file |
| Speed | Sufficient, but slows on large repos | Rust-based, reportedly fast |
| Rule coverage | Thousands of rules including plugins | About 491 (growing) |
| Ecosystem maturity | 10+ years | Relatively new |
For large existing codebases with heavy ESLint plugin dependence, switching costs are high. For new projects or simpler rule needs, Biome's single binary lowers consensus cost.
8. Common pitfalls
Formatter and linter colliding on the same rule — let the formatter own formatting and keep the linter on semantic checks. eslint-config-prettier standardizes that split.
Moving to ESLint 9's flat config (eslint.config.js) breaks compatibility with the older .eslintrc.*. Mixing both leaves one side ignored.
Black vs Ruff Format — the line-break decisions are nearly identical but not perfectly aligned. Pick one within a repo and stay consistent.
IDE auto-format vs CI format check on different versions — endless format-only diffs land on every PR. Pin versions in lockfile, package.json, and pyproject.toml.
Too many pre-commit hooks — bypassing (--no-verify) creeps in. Move heavy checks to CI.
Closing thoughts
The split between linter and formatter coexists with the single-binary trend (Biome, Ruff). With heavy ecosystem dependence, ESLint + Prettier; for new projects or speed-driven needs, Biome / Ruff. Either way, pinned versions, editor auto-apply, and CI verification together remove format noise from PRs.
Next
- python-venv-poetry-history
- regex
References include ESLint docs, Prettier docs, Biome, Ruff docs, Black docs, Spotless GitHub, typescript-eslint, editorconfig.org, and Prettier — Why Prettier?.