Introduction — The Formatter Debate Heats Up Again
In the first half of 2026, code formatters became a talking point again on GeekNews and Hacker News. As "opinionated" tools like gofumpt — stricter than gofmt — drew attention, and as ruff, the ultra-fast Python tool written in Rust, effectively became the standard, the old debate flared back up: "is it good for a tool to decide our style for us?"
This debate is not new, of course. Tabs or spaces, brace on the next line or not, semicolons or none. These questions are as old as software history and have spawned fierce religious wars in every team. But over the past decade the tide turned. The philosophy that "format is decided by the tool, and people do not argue" has won. This article looks at why that victory happened, and what philosophical choices the tools in each language ecosystem made.
The core claim is this. The value of a good formatter is not because the style it produces is "correct," but because it eliminates the debate about style itself. Opinionated tools take away choices and, in exchange, give back cognitive freedom.
What Does a Formatter Solve
To understand a formatter's value, imagine a world without one. Five teammates each write code with different indentation, different line-break rules, different brace styles. A Pull Request's diff mixes real changes with style changes, making review hard, and a large chunk of review time is wasted on meaningless nitpicks like "please add one more space here."
A formatter solves this problem at its root. The moment you save, or the moment you commit, all code is converted into one prescribed format. What you gain amounts to three things.
- **End of debate**: style is no longer a topic of discussion. The tool decides.
- **Consistency**: the whole repository looks as if written by one person. The cognitive load of reading the codebase drops.
- **Clean diffs**: because format is always normalized, only meaningful changes remain in the diff.
The third is especially important. When style changes do not pollute the diff, reviewers can focus solely on actual logic changes.
The Spectrum of Opinionation — Strictness vs Flexibility
Not all formatters share the same philosophy. Tools sit differently along the axis of "how much choice to give the user."
At one end is gofmt. It has almost no configuration. The Go team's decision was clear: "arguing about format is a waste of time. Set one right answer and have everyone follow it." Having no options is itself a feature. Nobody fights over a `.gofmt` config file.
At the other end are traditional tools. They offer hundreds of options so teams can assemble whatever style they want. Flexible, but with a cost. When there are many options, arguments break out again over those very options.
Summarizing this spectrum in a table:
| Tool | Language | Philosophy | Configurability |
| --- | --- | --- | --- |
| gofmt | Go | Minimal options, one right answer | Almost none |
| gofumpt | Go | Stricter than gofmt | None (stronger) |
| prettier | JS/TS, etc. | Opinionated but some options | A few options |
| black | Python | Uncompromising formatting | Very few |
| ruff | Python | Ultra-fast, format plus lint | Broadly adjustable |
The interesting case is gofumpt. It is a tool made by people who felt gofmt was "not strict enough." It removes even the tiny freedoms gofmt allows, producing more consistent results. It stepped one more foot in the "opinionated" direction.
A Look Inside Each Tool's Philosophy
gofmt and gofumpt — Go's Zero Tolerance
Go made format part of the language culture from the start. The `go fmt` command ships in the standard toolchain, and virtually all Go code looks identical. Put this code through gofmt,
package main
func main(){
x:=1
fmt.Println(x)
}
and it is always normalized to this.
package main
func main() {
x := 1
fmt.Println(x)
}
Indentation is tabs, imports are sorted, even brace position is fixed. gofumpt goes further, layering additional rules like removing unnecessary blank lines and enforcing certain idioms.
prettier — The Unifier of the Web Ecosystem
prettier unified the whole web ecosystem — JavaScript, TypeScript, CSS, Markdown, and more. It offers only a handful of options (line length, semicolons, quote style) and lets the tool decide the rest. This "opinionated but not fully dictatorial" balance drove broad adoption.
black — Python's No Compromise
black's slogan is famous: "any color you like, as long as it is black." There is almost nothing the user can adjust. You can change roughly the line length, and everything else is fixed. This extreme simplicity earned a strong response in the Python community.
ruff — Speed That Changed the Game
ruff is written in Rust and runs tens of times faster than existing Python tools. On top of that, it unifies linting and formatting into one tool. Speed is not mere convenience. Because it runs instantly on every save and every commit, developers barely notice the tool exists. When friction disappears, adoption gets easy.
The Difference Between Formatters and Linters
Here we should clarify a frequently confused distinction. Formatters and linters are different.
- A **formatter** changes the *shape* of code. Indentation, line breaks, whitespace. It never changes the code's meaning.
- A **linter** warns about the *content* of code. Unused variables, potential bugs, anti-patterns, convention violations.
Seeing the difference in a table makes it clear.
| Aspect | Formatter | Linter |
| --- | --- | --- |
| Target | Shape of code | Content and quality of code |
| Examples | Indentation, line breaks | Unused variables, null risk |
| Auto-fix | Always (shape only) | Some (only the safe ones) |
| Meaning change | None | Possible (careful when fixing) |
| On failure | Reformat | Warn or error |
The boundary sometimes blurs. Tools like ruff that unify linter and formatter are increasing. But conceptually it is safer to keep them distinct. A formatter's changes are always safe, whereas a linter's auto-fix can sometimes change meaning and needs review.
CI Enforcement and pre-commit — Where Automation Sits
For a formatter to show its real power, it must be enforced. If a developer "forgets" and does not format, consistency breaks. There are roughly three places to enforce it.
developer editor local commit CI pipeline
+---------------+ +---------------+ +---------------+
| format on save| --> | pre-commit | --> | fmt --check |
| (instant fb) | | (block commit)| | (block merge) |
+---------------+ +---------------+ +---------------+
softest middle gate last line
Ideally you defend by overlapping all three. Format on editor save has the least friction, the pre-commit hook prevents badly formatted code from being committed, and CI is the last line that keeps it from passing under any circumstance.
In CI you usually do not "apply" format but only "check" it. Like this.
Fail with a non-zero exit code if format is off
gofmt -l .
Or for Python
ruff format --check .
The `--check` mode does not fix files; it only determines "are there files that need fixing." If there are misformatted files, CI fails and the developer formats locally then pushes again. There is also a style where CI fixes the code itself and commits, but this creates unexpected commits and can be controversial, so proceed carefully.
An example pre-commit hook configuration:
repos:
- repo: local
hooks:
- id: format-check
name: format
entry: ruff format
language: system
types: [python]
Team Adoption Strategy
Introducing a new formatter into an existing codebase is trickier than it seems. The biggest obstacle is "the big reformat." When you first apply the tool, almost every file in the repository changes, and this enormous commit pollutes git blame history.
There are practical techniques to mitigate this.
- **Single reformat commit**: make one commit that changes only formatting, and never mix it with other changes.
- **Blame ignore configuration**: register that commit in the ignore file so git skips it in blame.
- **Avoid gradual adoption**: formatting file by file keeps diffs messy. Doing it all at once is better.
Summarizing the adoption order:
1. The team agrees on the tool and minimal configuration.
2. Perform a full reformat in a single commit.
3. Register that commit in the blame ignore file.
4. Enable pre-commit and CI checks.
5. Announce format-on-save to the team.
The Relationship Between Formatters and Code Review
A formatter's impact on code review is larger than you might think. As mentioned earlier, when format is automated, reviewers are freed from style callouts. This fundamentally changes the quality of review.
Imagine review on a team without a formatter. On every Pull Request, comments pile up like "the indentation differs here," "put this brace on the next line," "remove one space." Such callouts are usually correct but create no value. They only consume the reviewer's time and the author's patience. Worse, buried under these trivial callouts, the truly important problems — logic defects or design issues — get lost.
When a formatter removes this layer entirely, review focuses on the essence.
| Item | Without formatter | With formatter |
| --- | --- | --- |
| Style comments | Many | None |
| Diff noise | Large | Small |
| Review focus | Scattered | Logic/design |
| Emotional drain | Large | Small |
The "emotional drain" item is especially important. Style callouts are often taken personally. The author feels "my taste was rejected," and the reviewer becomes "the nagging person." When a tool takes on this conflict instead, review between people becomes far more constructive and collaborative. Nobody gets angry at gofmt.
This is the hidden value of formatters. Beyond just making code pretty, a formatter improves the team's human relationships. By removing the seed of debate, it lets people spend energy on the collaboration that truly matters.
The History of Format Wars — How We Got Here
Looking back briefly at the history up to the formatter's victory helps you understand the present better. Since the early days of software, developers fought over code style. This fight seems trivial but was surprisingly fierce.
The most famous debate is indentation. Tabs or spaces, and if spaces, two or four. This question divided developers for decades and became a staple of memes and jokes. Where to open braces, the max line length, the presence of semicolons were all the same.
The problem with such debates is that they have no objective right answer. Most style choices are pure taste, and neither side is clearly superior. Yet precisely because there is no right answer, the debate never ended. Everyone tried to justify their own taste, and teams burned energy without reaching consensus.
The turning point was two realizations. First, that consistency of style matters far more than the content of style. Whatever the style, you get most of the benefits as long as the whole team uses it consistently. Second, that keeping that consistency by hand is unrealistic. People make mistakes and forget. Only a tool can enforce perfect consistency.
These two realizations met, and the opinionated formatter was born. When gofmt declared "do not debate, settle on one," it was not merely a tool but a cultural declaration. And that declaration spread across the entire software world over the past decade. That we are free from style debates today is the result of this history.
Determinism and Idempotency — Conditions of a Good Formatter
A good formatter must have two technical properties: determinism and idempotency.
**Determinism** means the same input always produces the same output. Whichever developer's machine you run it on, whenever you run it, the result must be identical. If a formatter produces different results depending on the environment or the time, that is a disaster. If each teammate gets a different format, the very reason for using a formatter disappears.
**Idempotency** means reformatting already-formatted code does not change it. Formatting a once-formatted result again must leave it as is. Consider this.
source code --format--> result A --format--> result A (same)
|
if idempotency breaks
|
source code --format--> result A --format--> result B (different!)
A formatter whose idempotency is broken causes endless problems in CI checks. A developer formats locally and pushes, but the CI's formatter version differs subtly and reports that reformatting is needed again, falling into an infinite loop. That is why it is important for the whole team to pin the exact same version of the formatter.
Because of these two properties, formatter version management is a more important issue than it seems. Many teams explicitly pin the formatter version in the dependency file and force CI and local to use exactly the same version.
Performance in Large Codebases
If a formatter is slow, the development experience worsens. If it pauses for a few seconds on every save, developers want to turn it off. This reveals why a tool like ruff changed the game.
Traditional Python tools were written in Python itself and were slow. In contrast, ruff is written in Rust and runs tens of times faster. This speed difference goes beyond mere convenience. When a tool that runs on every save and every commit is instant, developers barely notice it exists, and when friction disappears, adoption happens without resistance.
Summarizing situations where performance decides adoption success or failure:
| Situation | Slow formatter | Fast formatter |
| --- | --- | --- |
| Auto-format on save | Perceived lag, want to turn off | Instant, unconscious |
| pre-commit hook | Wait on every commit | Momentary |
| Large-scale CI check | Pipeline bottleneck | Negligible |
| Editor integration | Disrupts typing | Smooth |
This is why the saying "speed is a feature too" comes up. No matter how good a tool's rules are, if it is slow it eventually gets turned off, and a turned-off tool has no value.
Format-Ignore Comments — The Dilemma of an Escape Hatch
Almost every formatter offers an ignore comment that excludes a certain region from formatting. It is an escape hatch for cases where auto-format actually hurts readability, like an aligned data table or deliberately arranged code.
But this escape hatch creates a dilemma. Without ignore comments, the formatter occasionally forces bad results; with ignore comments common, the core value of formatter consistency crumbles. When ignore comments are scattered throughout the repository, you end up in an unpredictable state of "which parts are formatted and which are not."
The practical principle is this. Use ignore comments truly rarely, only when there is a clear reason. And attach a short explanation of why you are ignoring. If ignore comments increase, that may be a sign not that the formatter is bad but that there is a problem with the code structure.
Cultural Differences Across Language Ecosystems
Interestingly, attitudes toward formatters differ by language ecosystem. These differences reflect each language's design philosophy and community culture.
The Go community accepted "one format" as culture from the start. Nobody complains that gofmt has no options; rather, they are proud of it. The JavaScript ecosystem, by contrast, long had great style diversity, and until prettier appeared, each team differed. Python was somewhere in between, then standardized rapidly with black and ruff.
These cultural differences give practical implications. When adopting a new language, understanding what its ecosystem's standard formatter is and what attitude the community holds lets you avoid unnecessary debates. In an ecosystem with a firm standard, following it is best; in one with a weak standard, it is important for the team to settle on one early.
Pitfalls of Automation and a Critical Perspective
Formatters are mostly a blessing, but there are pitfalls.
**Backlash from excessive strictness.** When a tool is too strict, it sometimes forces results that hurt readability. For example, a formatter may scatter code that was deliberately aligned like a table. Most tools provide a format-ignore comment for such cases, but overusing it dissolves the tool's value.
**Endless expansion of linter rules.** Formatters end debates, but linters can instead increase them. The team argues endlessly over "should we turn this rule on or off." When the ruleset bloats, developers grow numb to warnings and ignore even the truly important ones.
**The illusion that tools replace thinking.** Pretty format does not mean good code. A formatter only tidies shape; it does not improve design. Relying on the tool and thinking "it passed, so it is fine" makes you miss the real problems.
**The burden of managing config files.** The more flexible the tool, the larger the config file, and that config itself becomes a maintenance target. This is why configuration-free tools like gofmt are attractive. If there is no config to manage, there is nothing to fight over.
Formatting in Multi-Language Repositories
Many modern repositories are not in a single language. Backend in Go, frontend in TypeScript, infrastructure scripts in Python, config in YAML and JSON. In such multi-language repositories, the problem arises of coordinating a different formatter for each language.
Since each language's standard formatter differs, you cannot format the whole repository with one tool. Instead you gather the per-language formatters behind one unified entry point.
repository format command (one)
|
+--> Go files --> gofmt
+--> TS files --> prettier
+--> Python --> ruff
+--> YAML/JSON --> prettier
The important thing in this structure is that developers do not need to memorize a different command per language. A tool like the pre-commit framework looks at the file type and automatically calls the right formatter. The developer just commits, and behind the scenes each file gets formatted by the correct tool.
The pitfall of multi-language repositories is that tool version management becomes complex. You must pin the formatters of several languages each and match them across CI and local, so the number of management points grows. For this, it is good to keep a manifest that declares tool versions in one place and have every environment reference it.
The Three Tiers of Linter Rules
To use a linter well, you should not just turn on every rule but think in tiers. In practice, linter rules split roughly into three tiers.
- **Error tier**: things that are almost certainly bugs. Unused variables, unreachable code, wrong comparisons. Enforcing these is right.
- **Warning tier**: things that are usually bad but have exceptions. High-complexity functions, long parameter lists. Keep them as warnings but do not enforce.
- **Taste tier**: things that are purely a matter of preference. These are usually better left off. Turning them on only creates debate.
Summarizing these tiers in a table:
| Tier | Examples | Handling |
| --- | --- | --- |
| Error | Unused variables, null risk | Forced failure in CI |
| Warning | High complexity, long function | Show but pass |
| Taste | Preference for certain syntax | Usually disabled |
The key is to enforce only the error tier and handle the rest carefully. If you enforce every rule as an error, developers get tripped up by trivialities and soon try to bypass the linter itself. The smaller and clearer the ruleset, the more it is respected.
Introducing It to a Legacy Codebase
The discussion so far assumed a new project. But in reality many teams handle a legacy codebase where styles have been mixed for years. Introducing a formatter here is a harder problem.
The biggest fear is the worry that a big reformat will break something. A formatter changes only shape, so in principle it does not change behavior, but there are extremely rare exceptions. For example, subtle differences can arise in whitespace inside strings or in certain macro handling. So running the full test suite after introduction to confirm no regressions is essential.
A safe order for legacy introduction:
1. Install the formatter but do not change any files yet.
2. Run in check mode to grasp the scale of how many files will change.
3. Confirm the test suite is sufficient. If lacking, reinforce it first.
4. Perform the full reformat in a single commit.
5. Run the full tests to confirm there are no regressions.
6. Register that commit in the blame ignore file.
The importance of the test suite becomes clear here. If tests are solid, they immediately catch it when a big reformat breaks something. If tests are weak, a big reformat becomes a gamble. So paradoxically, the prerequisite for formatter introduction is often test coverage.
Another realistic consideration is in-progress branches. A big reformat commit touches almost every file, so it causes large conflicts with branches that are not yet merged. So it is good to do the big reformat at a moment with little in-progress work (e.g., right after a release), and announce it to all teammates in advance so they clean up their branches.
What a Formatter Cannot Do — Naming and Structure
It is important not to overestimate a formatter's power. A formatter tidies the shape of code but does not improve the quality of code. Consider these two functions.
def f(x, y, z):
a = x + y
b = a * z
return b
This code is perfectly formatted. The indentation and whitespace are impeccable. But the function name `f` and the variable names `a`, `b` explain nothing. A formatter will never fix this problem. Real design problems like naming, function decomposition, and abstraction levels are the domain of human judgment.
This is why you should not blindly trust formatters and linters. If passing the tool plants the illusion that the code is "good enough," the very thinking about the important naming and structure disappears. The tool guarantees only a lower bound on shape; it cannot raise the upper bound on design. Good code is still made by people.
Practical Recommendations
Compressing everything so far into practical guidance:
- If the language has a standard formatter, use it as is. Do not reinvent the wheel.
- Keep configuration minimal. The more options you add, the more debate you get.
- Distinguish formatter and linter conceptually so each is faithful to its role.
- In CI, only check — do not apply.
- Adopt with a single reformat commit, together with blame ignore.
- Keep linter rules a small elite set to prevent alert fatigue.
- Remember the tool only tidies shape; it does not substitute for design.
Editor Integration — The Last Piece That Removes Friction
For a formatter to settle into a team, it must integrate smoothly into the editor, the developer's everyday tool. The ideal experience is that developers barely notice the formatter exists. You write code and save, and it is tidied automatically. No need to type a separate command, no need to memorize rules.
The core method of editor integration is format on save. The moment you save a file, the formatter runs and normalizes the code. This lets developers focus solely on logic without worrying about style.
write code (regardless of style)
|
v
save (Ctrl+S / Cmd+S)
|
v
formatter runs automatically
|
v
normalized code on screen
The important thing here is that the whole team shares the same editor settings. If you commit an editor settings file to the repository, a new teammate gets the correct format settings the moment they open the repo. This prevents the confusion of "it formats differently in my editor."
Format on save has caveats, though. Saving can slow down on very large files, and there are cases where you open someone else's code, fix just one line, but the whole file gets reformatted and the diff grows. The latter is mostly solved by pre-normalizing the repository with the single reformat commit mentioned earlier.
The Future of Formatters — Integration and Intelligence
The evolution direction of formatter tools is summarized in two words: integration and intelligence.
The **integration** trend is shown well by ruff. Formerly, the formatter, linter, and import sorter were each separate tools. You had to install, configure, and attach each to CI. ruff merged these into one, simplifying configuration and execution. The fewer the tools, the less the management burden and the higher the consistency. This integration trend is likely to spread to other language ecosystems too.
The **intelligence** trend is still early. Traditional formatters are purely rule-based. But there is discussion of a direction where AI learns code style, automatically grasps a team's conventions, and matches even subtle styles that are hard to specify as rules. However, the formatter's fundamental requirements of determinism and idempotency are an obstacle here. AI's probabilistic nature conflicts with the principle of "same output for same input."
So for the time being, the core of formatters will still be deterministic rule-based. AI is likely to stay in an auxiliary role, suggesting rules or handling exceptional cases. For formatters in particular, predictability is a far more important value than flexibility.
Closing
The rise of opinionated formatters shows the maturity of software culture. We learned that style debates produce no value, and by delegating those debates to tools we became able to focus on what truly matters. The "no options" philosophy gofmt started has now spread across many language ecosystems.
But a tool removing debate does not mean it removes judgment. A formatter fixes shape and a linter warns of traps, but good design and clear code are still the human's job. Leaving to the tool what the tool does well, and spending human energy where the tool cannot reach — this is the real gift opinionated tools give us.
Let us take the time we would spend fighting over style and spend it making better software instead.
References
- Hacker News: https://news.ycombinator.com/
- GeekNews (Hada): https://news.hada.io/
- gofmt (Go official): https://pkg.go.dev/cmd/gofmt
- gofumpt repository: https://github.com/mvdan/gofumpt
- Prettier official docs: https://prettier.io/docs/en/
- Black official docs: https://black.readthedocs.io/
- Ruff official docs: https://docs.astral.sh/ruff/
- pre-commit framework: https://pre-commit.com/
현재 단락 (1/204)
In the first half of 2026, code formatters became a talking point again on GeekNews and Hacker News....