Filter concepts by levelShowing all levels.

Python · Code Quality and Tooling

Formatting and linting

Concepts
3

PEP 8 as the style guide, Black/isort/Ruff/Flake8 as the tools that enforce it automatically, and pre-commit as the mechanism that runs them before every commit — verified against real, intentionally messy Python files in this environment.

Style tools

Formatters and linters that automatically enforce PEP 8, and the git-hook framework that runs them before every commit.

PEP 8 and code formatters (Black, isort)

corebeginner

PEP 8 is Python's official style guide — spacing, naming, line length. Black and isort are tools that automatically reformat code to follow style rules, so a team never has to debate spacing or import order by hand.

Think of it as

PEP 8 is a style guide someone has to remember and apply by hand. Black and isort are that same guide, enforced automatically — a formatter never has an opinion to argue with, it just rewrites the file the same way every time.

python
black myfile.py
isort myfile.py
# or, check without writing (for CI):
black --check myfile.py

What we're doing: Run black on a real, deliberately messy file and see exactly what it rewrites.

before_and_after.shpython
black messy.py
# before: def calculate_total( items,tax_rate ):
# after:  def calculate_total(items, tax_rate):
#
# before: total=0
# after:  total = 0
#
# before: total+=item['price']
# after:  total += item["price"]
1
One command rewrites the whole file — spacing around parentheses, operators, and quote style all normalized at once.
Output
reformatted messy.py

All done! ✨ 🍰 ✨
1 file reformatted.

Why this works: black applies its own fixed style — single quotes become double, extra whitespace inside parentheses is removed, spacing around operators is normalized — deterministically, the same way every time, on any file, with almost no configuration to disagree about.

Manually formatting code that a formatter already owns

Wrong

python
def f(a,b):          # manually spaced by the author
    return a+b

# reviewer requests changing spacing style by hand in review comments

Better

python
# run black (or ruff format) before every commit -- automatically:
def f(a, b):
    return a + b

# code review then focuses on LOGIC, never spacing

What you see: Code review time gets spent debating spacing and quote style — a fully automatable, zero-judgment decision — instead of actual logic.

Why: A formatter removes style from the set of things a human ever needs to think about or argue over — running it as a required pre-commit or CI step means every file always matches the same style, with zero manual effort or debate.

Two formatters, two separate jobs

messy.py

unsorted imports, ragged spacing

isort

sorts & groups imports only

black

spacing, quotes, line breaks

  1. messy.py — unsorted imports, ragged spacing
  2. isort — sorts & groups imports only
  3. black — spacing, quotes, line breaks

Formatters — what each one rewrites

Formatters — what each one rewrites
ToolRewrites
blackspacing, quote style, line breaks, trailing commas
isortimport statement order and grouping
ruff formatthe same as black, built into the same tool as ruff's linter

Together

python
import os
import sys
import json
def calculate_total( items,tax_rate ):
    total=0
    for item in items:
        total+=item['price']
    return total*(1+tax_rate)

Remember: PEP 8 is the style guide; black and isort enforce it automatically — run them before every commit so style is never a manual decision or a review debate.

See also: ruff and flake8 · pre commit hooks

Ruff and Flake8

coreintermediate

Ruff is a very fast linter (and formatter) that reimplements most of Flake8's rules plus many others, in one tool. Flake8 is the older, plugin-based linter it is increasingly replacing — still common in existing projects.

Think of it as

Flake8 is a toolbox where each tool (a plugin) is a separate purchase and separate configuration. Ruff is the same job done by one fast, built-in multi-tool — most of the same checks, one install, one config block.

python
ruff check myfile.py         # lint
ruff check --fix myfile.py   # lint AND auto-fix
ruff format myfile.py        # format (Black-compatible)

What we're doing: Run both Ruff and Flake8 on the same flawed file and compare their real output shapes.

ruff_vs_flake8.shpython
ruff check messy.py
# I001 Import block is un-sorted or un-formatted
# F401 `os` imported but unused
# F841 Local variable `y` is assigned to but never used

flake8 messy.py
# F401 'os' imported but unused
# E225 missing whitespace around operator
# F841 local variable 'y' is assigned to but never used
1
ruff check catches import sorting (I001) in the same pass — flake8 needs a separate isort run for that.
6
flake8 catches E225 (missing whitespace) that ruff check does not flag by default, since ruff treats spacing as a FORMATTER concern (ruff format), not a linter one.
Output
Found 5 errors.
[*] 4 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).

Why this works: Ruff bundles import sorting, unused-code detection, and dozens of other checks into one pass; Flake8 relies on separately maintained plugins (pyflakes, pycodestyle) with their own scope — the two tools overlap heavily but are not identical, especially around spacing, which Ruff treats as the formatter's job.

Running Ruff and expecting it to catch every Flake8 spacing rule

Wrong

python
def calculate_total(items,tax_rate):   # missing space after comma
    total=0                            # missing space around =
    return total

# ruff check alone does NOT flag E231/E225 -- these are FORMATTING

Better

python
ruff check myfile.py     # catches logic/import issues (F-codes, etc.)
ruff format myfile.py    # catches spacing issues (Black-compatible)
# together, equivalent coverage to flake8 + black

What you see: A CI pipeline running only ruff check passes even though the code has inconsistent spacing that flake8 (or black) would have flagged.

Why: Ruff deliberately splits linting (ruff check, logic/style rules) from formatting (ruff format, whitespace/quotes) — spacing issues are the formatter's job, not the linter's, which is a different division of labor than Flake8 + separate plugins uses.

ruff check vs. flake8

ruff check

  • +Written in Rust — measurably much faster
  • +One tool: import sorting, unused code, hundreds of rules
  • +Spacing is ruff format's job, not check's

flake8

  • Plugin-based — pyflakes (F-codes), pycodestyle (E-codes)
  • Needs a separate tool (isort) for import order
  • Catches spacing (E225) directly, in the same pass
  • ruff check
    • Written in Rust — measurably much faster
    • One tool: import sorting, unused code, hundreds of rules
    • Spacing is ruff format's job, not check's
  • flake8
    • Plugin-based — pyflakes (F-codes), pycodestyle (E-codes)
    • Needs a separate tool (isort) for import order
    • Catches spacing (E225) directly, in the same pass

Same bug, two linters' output shape

Same bug, two linters' output shape
IssueRuff codeFlake8 code
Unused importF401F401 (via pyflakes)
Unused variableF841F841 (via pyflakes)
Unsorted importsI001not detected (needs isort separately)
Missing whitespace around operatorE225E225 (via pycodestyle)

Together

python
import os  # unused

def f(a,b):
    total=a+b
    return total

Remember: Ruff is fast and all-in-one, covering most of Flake8's rules — but spacing is ruff format's job, not ruff check's.

See also: pep8 and formatters · mypy and pyright

Pre-commit

standardintermediate

pre-commit is a framework that runs configured checks automatically every time someone runs git commit — the commit is blocked until every check passes, so bad formatting or an obvious lint error never reaches a shared branch.

Think of it as

Without pre-commit, code quality checks run in CI, after a push — the mistake is already shared. pre-commit is a checkpoint at the very last local step, catching the same issues before they ever leave the developer's machine.

python
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

# then:
pip install pre-commit
pre-commit install   # registers the git hook
git commit -m "..."  # hooks run automatically here

Remember: pre-commit runs configured checks on every git commit, blocking it until they pass — issues never reach a shared branch.

See also: ruff and flake8 · pep8 and formatters

Advertisement