How do linters and formatters like ruff, flake8, and black fit into a Python workflow?
Quick Answer
**Formatters** (`black`, and `ruff format`) auto-rewrite code to a consistent style, eliminating style debates and manual formatting effort entirely. **Linters** (`flake8`, `pylint`, `ruff check`) statically analyze code for likely bugs, unused imports/variables, and style violations without changing the code themselves. `ruff` is a newer Rust-based tool that combines both linting and formatting (compatible with `black`'s style) at dramatically higher speed than the older Python-based tools it's replacing.
Detailed Answer
Formatters: no more style debates
# before black
def f(x,y ,z):
return{"a":x,'b':y,"c" :z}
# after `black .`
def f(x, y, z):
return {"a": x, "b": y, "c": z}
black (and ruff format, which implements a compatible style) rewrite
code automatically to one consistent format — quote style, spacing, line
length, trailing commas. The point isn't that this particular style is
objectively best; it's that a team stops spending review time and mental
energy on formatting bikeshedding entirely, since the tool decides and
everyone's code converges to the same look.
Linters: catching likely bugs and code smells
import os # F401: 'os' imported but unused
import sys
def process(items):
result = []
for item in items:
result.append(itme.value) # F821: undefined name 'itme' (a typo!)
return reuslt # F821: undefined name 'reuslt' (another typo!)
flake8/pylint/ruff check statically scan code for problems that
wouldn't necessarily crash immediately (an unused import) or would crash
only when that code path actually runs (a typo in a rarely-exercised
branch) — catching these at commit/CI time instead of in production.
Rule sets typically cover unused imports/variables, undefined names,
common bug patterns (except: bare clauses, mutable default arguments),
and complexity/style conventions (PEP 8 line length, naming).
ruff: the modern consolidation
ruff check . # lint (replaces flake8, isort, pyupgrade, and more, via one tool)
ruff format . # format (black-compatible)
ruff is written in Rust and reimplements the rules of dozens of
previously-separate Python linting/formatting plugins (flake8 plus its
common plugin ecosystem, isort for import sorting, pyupgrade for
modernizing syntax) in a single binary that runs 10-100x faster than the
pure-Python tools it replaces — this speed matters enough in practice
that ruff has become the default choice for new projects, often
alongside or instead of both flake8 and black.
Fitting into a workflow: pre-commit and CI
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0
hooks:
- id: ruff
- id: ruff-format
The standard setup runs linting/formatting automatically at commit time
(via pre-commit) so problems are caught before they even reach a PR, and
again in CI as a required check — so no code merges without passing both
the formatter's consistency check and the linter's static analysis.
Static analysis vs type checking: complementary, not overlapping
Linters catch things like unused variables and style; mypy/pyright
(covered separately) catch type mismatches — both typically run in the
same CI pipeline, each covering a different class of problem that the
other doesn't.
Interview-ready summary: Formatters (black/ruff format) eliminate
style debates by auto-rewriting code to one consistent look; linters
(flake8/ruff check) statically catch likely bugs and code smells
without changing code. ruff consolidates most of this Python tooling
ecosystem into one fast Rust-based tool, and both are typically wired
into pre-commit hooks and CI so no code merges without passing them.