Automated Refactoring with Black
What Black does
Section titled “What Black does”Black is an opinionated formatter that:
- rewrites code formatting automatically
- enforces consistent style
black .Check only (CI mode):
black --check .Configure line length
Section titled “Configure line length”In pyproject.toml:
[tool.black]
line-length = 88Run Black before linting so style issues disappear.
One file, six tools
Section titled “One file, six tools”Every measurement on this page — and on the other five tools in this phase — comes from running the tool against this deliberately flawed file:
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)| tool | what it reported on sample.py |
|---|---|
| flake8 | 5 style and dead-code issues. No security findings. |
| pylint | 4 issues, score 8.10/10 |
| mypy | 1 type error, which neither linter saw |
| bandit | 5 security issues, 3 of them HIGH |
| radon | complexity 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.
Black does not refactor. It reformats.
Section titled “Black does not refactor. It reformats.”The page title is the industry’s word, not an accurate one. Black rewrites layout and nothing else — measured on a messy function:
-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 + bonusSpaces after commas, no spaces around a keyword default, spaces around a binary operator. What it did not change:
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.
flowchart TD B["black"] --> L["line breaks, quotes, spacing, trailing commas"] B --> X["NOT names, structure, complexity or behaviour"] L --> D["every file looks the same"] D --> R["code review stops discussing layout"]
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.
In CI, use --check
Section titled “In CI, use --check”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.
repos:
- repo: https://github.com/psf/black
rev: 26.5.1
hooks:
- id: blackPin the version. A different black version can reformat files differently, which turns into a diff nobody asked for on an unrelated pull request.
Making it agree with flake8
Section titled “Making it agree with flake8”Measured — black-formatted code fails default flake8:
fmt.py:1:80: E501 line too long (84 > 79 characters)Black targets 88 columns; flake8 defaults to 79:
[flake8]
max-line-length = 88
extend-ignore = E203, W503Adopting it on an existing project
Section titled “Adopting it on an existing project”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:
git blame --ignore-rev <sha>
echo "<sha>" >> .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revsCheck yourself
Section titled “Check yourself”-
Running black over a function with cyclomatic complexity C (15) left it at C (15). What does that show?
Formatting and complexity are independent. Confusing them is how a codebase ends up beautifully formatted and still unmaintainable.
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.
-
What does black --check do?
Measured exit 1 before formatting and 0 after. That is what makes it usable in CI and pre-commit hooks.
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.
-
Why pin black's version in a pre-commit config?
Formatting churn from a version drift is noise in review, and it is avoidable by pinning.
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.coffeeCtapch.feedbackHeading
pch.feedbackSubheading