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. 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.
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.