Skip to content

Automated Refactoring with Black

Black is an opinionated formatter that:

  • rewrites code formatting automatically
  • enforces consistent style
bash
black .

Check only (CI mode):

bash
black --check .

In pyproject.toml:

toml
[tool.black]
line-length = 88

Run Black before linting so style issues disappear.

Every measurement on this page — and on the other five tools in this phase — comes from running the tool against this deliberately flawed file:

sample.py
import os
import subprocess
import hashlib
 
 
def process(items, user_input, flag = False):
    unused_var = 42
    result=[]
    for i in items:
        if i > 0:
            if flag:
                if i % 2 == 0:
                    result.append(i*2)
                else:
                    result.append(i)
            else:
                result.append(i)
    password = "hunter2"
    h = hashlib.md5(password.encode()).hexdigest()
    os.system("echo " + user_input)
    subprocess.call("ls " + user_input, shell=True)
    return result
 
 
def add(a: int, b: int) -> int:
    return a + b
 
 
x = add("1", 2)
toolwhat it reported on sample.py
flake85 style and dead-code issues. No security findings.
pylint4 issues, score 8.10/10
mypy1 type error, which neither linter saw
bandit5 security issues, 3 of them HIGH
radoncomplexity A (5), maintainability A (56.30)

The headline is that no tool subsumes another. flake8 read the whole file and reported nothing about the shell injection on line 20. bandit read the same file and said nothing about the type error on line 29. Running one and concluding the code is clean is the mistake this phase exists to prevent.

sketch Six tools, one file, almost no overlap p5.js
Each tool was run against the same 29-line file. Click a tool to see what it found and, more importantly, what it did not.

The page title is the industry’s word, not an accurate one. Black rewrites layout and nothing else — measured on a messy function:

black --diff messy.py
-def grade(score,bonus = 0,curve=False,late=False,extra_credit=None,retake=False):
-    total = score+bonus
+def grade(score, bonus=0, curve=False, late=False, extra_credit=None, retake=False):
+    total = score + bonus

Spaces after commas, no spaces around a keyword default, spaces around a binary operator. What it did not change:

radon cc, before and after formatting
before:  F 1:0 grade - C (15)
after :  F 1:0 grade - C (15)

Identical. The function was hard to reason about before and is exactly as hard afterwards — it just looks tidier. Formatting and complexity are independent, and confusing the two is how a codebase ends up beautifully formatted and unmaintainable.

diagram Diagram mermaid

The real benefit is that it ends an argument

Section titled “The real benefit is that it ends an argument”

Black is deliberately almost unconfigurable. There is no option for two-space indents or single quotes, and that is the feature: nobody can propose a house style, so review comments are about the code.

exit codes, measured
black --check fmt.py     # exit 1   — needs formatting
black fmt.py             # rewrites the file
black --check fmt.py     # exit 0   — already formatted

--check changes nothing and signals through the exit code, which is exactly what a CI step or a pre-commit hook needs.

.pre-commit-config.yaml
repos:
  - repo: https://github.com/psf/black
    rev: 26.5.1
    hooks:
      - id: black

Pin the version. A different black version can reformat files differently, which turns into a diff nobody asked for on an unrelated pull request.

Measured — black-formatted code fails default flake8:

flake8 on a black-formatted file
fmt.py:1:80: E501 line too long (84 > 79 characters)

Black targets 88 columns; flake8 defaults to 79:

setup.cfg
[flake8]
max-line-length = 88
extend-ignore = E203, W503

One commit that reformats everything makes git blame useless — every line’s last author becomes whoever ran black. Record that commit so blame can skip it:

ignore the reformat in blame
git blame --ignore-rev <sha>
echo "<sha>" >> .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revs
pch.quizTag pch.quizDefaultTitle
  1. Running black over a function with cyclomatic complexity C (15) left it at C (15). What does that show?

    pch.quizShowAnswer

    B — black changes layout only; complexity and structure are untouched — Formatting and complexity are independent. Confusing them is how a codebase ends up beautifully formatted and still unmaintainable.

  2. What does black --check do?

    pch.quizShowAnswer

    B — changes nothing and signals through the exit code: 1 if formatting is needed, 0 if not — Measured exit 1 before formatting and 0 after. That is what makes it usable in CI and pre-commit hooks.

  3. Why pin black's version in a pre-commit config?

    pch.quizShowAnswer

    B — different versions can format the same file differently, producing unrelated diffs on someone else's pull request — Formatting churn from a version drift is noise in review, and it is avoidable by pinning.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading