How do linters and formatters like ruff, flake8, and black fit into a Python workflow?

6 minbeginnertoolinglintingformattingruff

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.