Let’s keep in touch! Join me on the Javier Tiniaco Leyba newsletter 📩

Linters and Formatters in Software

Written in

by

Linters and formatters in software

In this blog post, the spotlight is on a family of tools that quietly shape how modern software is written: static analysis tools, especially linters and formatters. These tools read your code without ever running it, catching potential bugs, enforcing conventions, and polishing style long before anything reaches production. Linters act like an automated reviewer, flagging logical issues, risky patterns, and inconsistent practices, while formatters focus on the visual layer, relentlessly reshaping indentation, spacing, and line breaks into a clean, uniform style. Along the way, you’ll see how tools like ESLint, Black, Prettier, and SQLFluff fit into this ecosystem, why teams rely on them, where they can be frustrating, and how they complement alternatives such as type checkers and traditional code review. The goal is not just to define buzzwords, but to give you a practical mental model for when and how to use linters and formatters to make your codebase easier to read, safer to change, and more pleasant to work with.

What is a linter in software?

A linter analyzes source code against a set of rules to flag programming errors, potential bugs, security issues, and style problems. It typically parses the code into an abstract syntax tree and then checks patterns such as unused variables, unreachable code, or overly complex functions.

Linters exist for most languages (ESLint for JavaScript, Pylint for Python, etc.) and often integrate with IDEs, CI pipelines, and git hooks. One can frame linting as an automated “code reviewer” that never gets tired of pointing out risky or inconsistent code.

Examples of linters

  • ESLint (JavaScript/TypeScript) – The de facto standard linter in the JS ecosystem, highly configurable and extensible with plugins.
  • Pylint (Python) – Feature‑rich Python linter that checks for errors, code smells, and style issues against configurable rules.​
  • Flake8 (Python) – A lightweight but very common Python linter that combines several checks (PEP 8 style, complexity, etc.).
  • RuboCop (Ruby) – A popular Ruby static code analyzer and style checker used across many Ruby/Rails projects.
  • Checkstyle or SpotBugs (Java) – Checkstyle focuses on code style and conventions, while SpotBugs targets bug patterns; both are classic Java linters.

What is a formatter in software?

A formatter in software is a tool that automatically rewrites the appearance of your source code —things like indentation, spacing, line breaks, and bracket placement— without changing what the code actually does. It exists to enforce a consistent visual style across a codebase so humans can read and review code more easily.

A code formatter takes your valid but inconsistently styled code and outputs the same program in a clean, standardized layout. It focuses purely on presentation concerns (whitespace, line wrapping, alignment, quote style, capitalization, etc.), not on logic or semantics.

Examples of formatters

  • Prettier (JavaScript and more) – An opinionated formatter that supports JS/TS, HTML, CSS, Markdown, and more; very common in web projects.
  • Black (Python) – The “uncompromising” Python code formatter that enforces a single consistent style.
  • autopep8 or YAPF (Python) – Both are established Python formatters that reformat code to follow PEP 8 or a configured style.
  • gofmt (Go) – The standard Go formatter, effectively mandatory in the Go ecosystem and integrated into tooling by default.
  • clang-format (C/C++ and others) – A widely used formatter from the LLVM project for C, C++, Objective‑C, and more.

Static code analysis tools

So, both linters and formatters are static code analysis tools. A static analysis tool examines code without running it, looking for issues such as bugs, security vulnerabilities, and violations of coding or style rules. It works purely on the “static” artifact (source code, bytecode, config, etc.), unlike tests or profilers, which observe the program while it executes.

Both linters and formatters are generally considered types of static analysis tools, but with different focuses:

  • Linters are classic static analysis tools: they analyze code to detect potential errors, risky patterns, security issues, and style or convention violations before runtime. Their output is usually warnings or errors for the developer to fix.
  • Formatters also operate statically (they read and rewrite source code without running it), so they fit under a broad definition of static analysis, but they are typically classified as “formatting tools” or “code layout tools” whose purpose is to normalize style rather than find bugs.

Are linters and formatter identical concepts?

Linters and formatters are related but distinct tools. A linter’s primary goal is to detect issues in code quality and correctness, while a formatter’s primary goal is to enforce a uniform visual style. Formatters automatically reflow and reindent code —adjusting whitespace, line length, brackets, capitalization, and similar surface details — without altering runtime behavior. Many teams run both: the formatter handles layout, and the linter handles deeper issues and non‑trivial style rules.

In most modern usage, a formatter is not considered a linter; they are treated as two different kinds of static analysis tools with different goals, even though they both “look at” your code. A linter analyzes code to detect problems: potential bugs, unsafe patterns, and violations of coding conventions that can affect correctness, maintainability, or security. A formatter rewrites only the surface layout of the code —indentation, spaces, line breaks, brace placement— while preserving behavior.

Because of this, linters are evaluative (they report issues), whereas formatters are transformative (they change how the code looks but not what it does). That’s why many guides explicitly advise: use linters for correctness and best practices, and formatters solely for style.

Some tools are both linters and formatters, for example, take SQLFluff (SQL), a modern, dialect-flexible linter for SQL with extensive rule sets and templating support. Besides being a linter, SQLFluff can automatically reformat SQL to a chosen style, acting as a powerful SQL autoformatter too.

Purpose of linter and formatters

The core purpose of a linter is to improve code quality by catching problems early, ranging from simple syntax mistakes to patterns that are likely to lead to bugs or security vulnerabilities. Linters also enforce team or project standards so that code remains consistent and maintainable over time.

Formatters aim to remove bikeshedding over style by automating layout decisions, which speeds up development and simplifies code review. Together, they support a smoother workflow: developers focus on logic while tools handle consistency and many categories of preventable errors.

What problems do linters and formatters solve?

Linters address issues like:

  • Subtle bugs, such as unused variables, dead code, or suspicious comparisons.
  • Code smells, such as overly complex functions or patterns that violate best practices.
  • Security and reliability issues, including certain injection risks or unsafe APIs in more advanced linters.

Formatters solve problems like:

  • Inconsistent indentation, spacing, and line breaks across contributors that make code harder to read.
  • Noisy diffs in version control caused purely by style changes rather than logic changes.
  • Time spent manually aligning code, which formatters can handle instantly and repeatably.

Linter vs formatter characteristics

AspectLintersFormatters
Main goalFind bugs, risks, and bad practices.Make style consistent and readable.
Typical outputWarnings and errors to fix manually.Directly rewrites code layout.
Impact on logicMay suggest logic changes but does not auto‑rewrite by default.Must not change behavior, only formatting.
ConfigurationOften many rules and options.Usually fewer, more opinionated options.

Pros and Cons of linters and formatters

Pros of linters:

  • Catch issues early, reducing production bugs and improving reliability.
  • Encourage consistent idioms and best practices across a team.
  • Integrate with CI to prevent low‑quality code from being merged.

Cons of linters:

  • Can generate a large number of warnings, which may feel noisy or overwhelming.
  • Misconfigured rules can block useful patterns or slow teams down.
  • Rule sets require maintenance as languages and frameworks evolve.

Pros of formatters:

  • Enforce a uniform style automatically, which improves readability.
  • Reduce style debates and make diffs cleaner and easier to review.
  • Save time by handling indentation and layout with a single command or keyboard shortcut.

Cons of formatters:

  • Opinionated defaults may not match every developer’s preferences.
  • Automated line wrapping or layout can sometimes feel awkward for edge cases.
  • Combining multiple tools (e.g., formatter plus linter plus IDE settings) can require careful configuration

What are the alternatives to linters and formatters?

Several complementary or alternative approaches can cover similar goals:

  • Strong type systems and type checkers (e.g., TypeScript, mypy) catch entire classes of errors via static typing rather than lint rules.
  • Compiler warnings and built‑in analyzers provide basic checks without extra tools, especially in compiled languages.
  • Code review guidelines and style guides (like PEP 8 for Python) can be enforced socially instead of via automation, though this is slower and less consistent.
  • More heavyweight static analysis platforms (e.g., SonarQube and similar products) add deeper analysis, metrics, and dashboards beyond typical linting.

A concrete example: SQLFluff

I developed a horrendous SQL query to demonstrate what a linter could tell us about it. My purpose is to ensure I get warnings and errors from the linter so I can show its usage. There are plenty of things wrong with this SQL query, which I did on purpose to see what the linter tells me about it:

SQL
SElEct * from order_items where date between 20250101 and 20251130
)
select
    -- order_id as order, -- cannot use 'order' as alias, it's a reserved word
    order_id as ordernum,
    sum(order_amount) as amount
from
    cte join cte
group by 1 order By 2 desc

I will use SQLFluff command-line interface for simplicity. Note that for the command to work, you must have Python installed and SQLFluff module in the Python environment. If you are interested in running these commands or installing SQLFluff, you can follow this guide. The following command will output the issues the linter detects sqlfluff lint --dialect sparksql horrendous_query.sql

== [horrendous_query.sql] FAIL                                                                                                                   
L:   2 | P:   1 | CP01 | Keywords must be consistently lower case.
                       | [capitalisation.keywords]
L:   2 | P:   1 | LT02 | Expected indent of 4 spaces. [layout.indent]
L:   2 | P:  27 | LT14 | The 'where' keyword should always start a new line.
                       | [layout.keyword_newline]
L:   4 | P:   1 | LT08 | Blank line expected but not found after CTE closing
                       | bracket. [layout.cte_newline]
L:   6 | P:   5 | RF02 | Unqualified reference 'order_id' found in select with
                       | more than one referenced table/view.
                       | [references.qualification]
L:   7 | P:   9 | RF02 | Unqualified reference 'order_amount' found in select
                       | with more than one referenced table/view.
                       | [references.qualification]
L:   9 | P:   8 | LT02 | Expected line break and no indent before 'join'.
                       | [layout.indent]
L:   9 | P:   9 | AM05 | Join clauses should be fully qualified.
                       | [ambiguous.join]
L:   9 | P:   9 | AM08 | Implicit cross join detected.
                       | [ambiguous.join_condition]
L:   9 | P:  14 | AL04 | Duplicate table alias 'cte'. Table aliases should be
                       | unique. [aliasing.unique.table]
L:  10 | P:  12 | LT14 | The 'order' keyword should always start a new line.
                       | [layout.keyword_newline]
L:  10 | P:  18 | CP01 | Keywords must be consistently lower case.
                       | [capitalisation.keywords]
L:  10 | P:  27 | LT12 | Files must end with a single trailing newline.
                       | [layout.end_of_file]
All Finished 📜 🎉!

As expected, we get many warnings and errors. For each one, we get the line where the error appears (L), the position in the line (P), and the type of rule causing the error. For example, the first one is ( L: 2 | P: 1 | CP01 ) which indicate the error occurs on line 2, position 1 and its a capitalization error. Apparently because keywords must be in lowercase according to the coding style rules. Many of such errors are related to formatting, and not related with the content or logic of the query itself, so we can fix them by formatting the query with the following command sqlfluff format --dialect sparksql horrendous_query.sql which will format the file according to the style guide and overwritte it. The result of the command:

==== finding fixable violations ====
== [horrendous_query.sql] FAIL                                                                                                                              
L:   2 | P:   1 | CP01 | Keywords must be consistently lower case.
                       | [capitalisation.keywords]
L:   2 | P:   1 | LT02 | Expected indent of 4 spaces. [layout.indent]
L:   2 | P:  27 | LT14 | The 'where' keyword should always start a new line.
                       | [layout.keyword_newline]
L:   4 | P:   1 | LT08 | Blank line expected but not found after CTE closing
                       | bracket. [layout.cte_newline]
L:   9 | P:   8 | LT02 | Expected line break and no indent before 'join'.
                       | [layout.indent]
L:  10 | P:  12 | LT14 | The 'order' keyword should always start a new line.
                       | [layout.keyword_newline]
L:  10 | P:  18 | CP01 | Keywords must be consistently lower case.
                       | [capitalisation.keywords]
L:  10 | P:  27 | LT12 | Files must end with a single trailing newline.
                       | [layout.end_of_file]
== [query_to_be_linted.sql] FIXED
8 fixable linting violations found

Eight code style violations were fixed and now the query looks like this:

SQL
with cte as (
    select * from order_items
    where date between 20250101 and 20251130
)

select
    -- order_id as order, -- cannot use 'order' as alias, it's a reserved word
    order_id as ordernum,
    sum(order_amount) as amount
from
    cte
join cte
group by 1
order by 2 desc

That is definitely easier to read, which is great for mainteinability and readibility. So far, we have just used the formatter part of SQLFluff but there are still issues remaning not directly tied to the query format. For example this is the output of running again sqlfluff lint --dialect sparksql horrendous_query.sql :

== [horrendous_query.sql] FAIL                                                                                                                              
L:   8 | P:   5 | RF02 | Unqualified reference 'order_id' found in select with
                       | more than one referenced table/view.
                       | [references.qualification]
L:   9 | P:   9 | RF02 | Unqualified reference 'order_amount' found in select
                       | with more than one referenced table/view.
                       | [references.qualification]
L:  12 | P:   1 | AM05 | Join clauses should be fully qualified.
                       | [ambiguous.join]
L:  12 | P:   1 | AM08 | Implicit cross join detected.
                       | [ambiguous.join_condition]
L:  12 | P:   6 | AL04 | Duplicate table alias 'cte'. Table aliases should be
                       | unique. [aliasing.unique.table]

Interesting. The linter is warning us about issues not related to the format, such as implicit cross joins, duplicate table aliases, unqualified column references (columns without their proper table prefix). Those are definitely issues not directly related to the query format but to its quality. This information is useful because one can act on it to improve code quality. SQLFluff is also a linter, so it shows us such quality issues and might be able to fix some of them on its own. Let’s try it with the fix commands sqlfluff fix --dialect sparksql horrendous_query.sql :

=== finding fixable violations ====
== [query_to_be_linted.sql] FAIL                                                                                                                              
L:  12 | P:   1 | AM05 | Join clauses should be fully qualified.
                       | [ambiguous.join]
== [query_to_be_linted.sql] FIXED
1 fixable linting violations found
  [4 unfixable linting violations found]

Ok, so it was able to fix one of the five issues. It is our job to fix the remaining four. So, that’s an example of how a tool can be both a formatter and a linter, and how it helps us identify common issues in code, so that we can address them quickly and improve code quality and mainteinability.

Conclusion

To wrap up, linters, formatters, and other static analysis tools are best seen as teammates in your development workflow rather than fussy gatekeepers. Together they scan your code without running it, catching bugs early, enforcing shared conventions, and keeping style debates out of code review so humans can focus on design and behavior instead of bracket placement. Linters act as your automated reviewers, highlighting risky patterns, smells, and inconsistencies; formatters clean up the visual mess automatically; and specialized tools like SQLFluff show how a single tool can bridge both worlds for a specific domain. Used thoughtfully—tuned rules, clear team agreements, and sensible integration into editors and CI—they reduce friction, improve readability, and steadily raise the baseline quality of your codebase, letting you spend more energy on solving real problems rather than fixing preventable mistakes.

Let’s keep in touch! Join me on the Javier Tiniaco Leyba newsletter 📩

Leave a Reply

Discover more from Tiniaco Leyba

Subscribe now to keep reading and get access to the full archive.

Continue reading