cheat sheet
black
Format Python code consistently with black. Covers installation, configuration, editor integration, and how it compares to ruff format.
black — Python Code Formatter
What it is
Black is an uncompromising Python code formatter created by Łukasz Langa and now maintained by the Python Software Foundation. It deliberately offers almost no configuration options for style, producing deterministic output so that formatting is never debated in code review — what Black produces is the style. For new projects, ruff format implements Black-compatible formatting in a single faster tool; Black remains the standard in established projects and CI pipelines that adopted it first.
For new projects, consider ruff format instead — it implements black-compatible formatting and combines linting in one tool. Black remains the gold standard and is still widely used in existing projects.
Install
pip install black
Output: (none — exits 0 on success)
Quick example
# messy.py (before black)
x = {'a':1,'b':2}
def foo(x,y,z):
return x+y+z
result=foo(1,2,3)
black messy.py
Output:
reformatted messy.py
All done! ✨ 🍰 ✨
1 file reformatted.
After black:
x = {"a": 1, "b": 2}
def foo(x, y, z):
return x + y + z
result = foo(1, 2, 3)
When / why to use it
- Existing projects already using black — keep using it, no migration needed.
- Teams that want zero configuration and a single authoritative style.
- When you want the black badge on your README to signal consistent formatting.
Common pitfalls
black reformats strings to double quotes — if your codebase uses single quotes extensively, the first run will produce a large diff. This is expected and only happens once. Commit the reformatted code and move on.
--checkvs no flag —black --checkreports whether files would change but does not change them (exit code 1 if any file would be reformatted). Runningblackwithout--checkmodifies files in place.
Use
# fmt: offand# fmt: oncomments to disable black for specific blocks (e.g. hand-aligned tables or magic number arrays). Use sparingly.
Richer example — check mode in CI
# CI: fail if code is not already black-formatted
black --check --diff src/
Output (when files need formatting):
--- src/utils.py 2026-04-25 09:00:00
+++ src/utils.py 2026-04-25 09:00:00
@@ -1,3 +1,6 @@
-def foo(x,y): return x+y
+def foo(x, y):
+ return x + y
+
would reformat src/utils.py
Oh no! 💥 💔 💥
1 file would be reformatted.
Output (when all files are formatted):
All done! ✨ 🍰 ✨
5 files would be left unchanged.
pyproject.toml configuration
Black has intentionally minimal config. The only common options:
[tool.black]
line-length = 88 # default; increase if your team prefers longer lines
target-version = ["py312"]
skip-string-normalization = false
exclude = '''
/(
\.git
| dist
| build
| \.venv
)/
'''
Editor integration
| Editor | Plugin |
|---|---|
| VS Code | Python extension → set "editor.defaultFormatter": "ms-python.black-formatter" |
| PyCharm | Settings → Tools → Black |
| Neovim | conform.nvim or null-ls |
| Emacs | blacken-mode |
Pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
pip install pre-commit
pre-commit install
Output: (none — exits 0 on success)
After this, black runs automatically before every commit. If it reformats any file the commit is aborted — re-stage the reformatted files and commit again.
Code style philosophy
Black's central claim is that formatting is not a question of taste — it's a question of consistency. By offering almost no knobs, it removes formatting debates from code review and frees mental cycles for actual code. The output is deterministic: given the same input file, every Black version of the same major release produces byte-identical output. The cost is that you may not love every choice (double quotes, two blank lines between top-level functions, magic-comma-driven line splits) — but every Python developer who has used Black recognises the layout instantly, and that recognition is the entire point.
Three properties define the style:
- Determinism — formatting is a pure function of source code; CI can re-format on any machine and get the same bytes.
- Diff minimisation — Black tries to keep diffs small. A magic trailing comma forces a line split that survives future edits; an explicit format respects intent over re-flowing the whole expression.
- Limited configuration —
line-length,target-version,skip-string-normalization, and file inclusion/exclusion. That's it. Everything else is non-negotiable.
If you find yourself wanting "Black, but with single quotes" or "Black, but with one blank line", you're working against the design.
skip-string-normalizationis the one concession to the single-quote camp; everything else is intentionally locked.
Configuration via pyproject.toml
Black reads configuration from [tool.black] in pyproject.toml. The schema is small. line-length is the soft wrap target — Black breaks long lines but won't force-shorten readable shorter ones. target-version is a list because Black can format for multiple Python versions simultaneously (it picks the lowest-common-denominator syntax). preview opts into experimental formatting changes that will become defaults in a future release.
[tool.black]
line-length = 100
target-version = ["py311", "py312"]
skip-string-normalization = false
skip-magic-trailing-comma = false
preview = false
unstable = false
# include/exclude accept a single regex; the verbose multi-line form
# is conventional for readability
include = '\.pyi?$'
extend-exclude = '''
/(
\.git
| \.venv
| build
| dist
| migrations
| _drafts
)/
'''
force-exclude = '^/generated/' # always excluded, even from explicit paths
extend-excludeadds to Black's default exclusions;excludereplaces them. Almost always useextend-exclude— otherwise you accidentally re-enable formatting on.git/,__pycache__/, etc.
target-version matters because Black uses newer syntax where available — for example, target-version = ["py310"] lets Black collapse Union[int, str] to int | str. Setting it too low blocks otherwise-desirable rewrites; setting it too high produces syntax errors on older runtimes.
--check, --diff, --quiet — read-only modes
black FILE modifies files in place. The three read-only flags let you preview or audit without writing. Combine them in CI to fail the build when files would change but never have CI mutate code.
# Report which files would change; exit 1 if any would
black --check src/
# Print a unified diff of every change Black would make
black --diff src/
# Both — fail with the diff for debugging
black --check --diff src/
# Suppress all output except errors (useful for hooks)
black --quiet src/
# Show what Black is doing on each file (verbose)
black --verbose src/
Output (--check --diff with a change pending):
--- src/utils.py 2026-05-25 09:00:00
+++ src/utils.py 2026-05-25 09:00:00
@@ -1,2 +1,4 @@
-def foo(x,y): return x+y
+def foo(x, y):
+ return x + y
+
would reformat src/utils.py
Oh no! 💥 💔 💥
1 file would be reformatted, 4 files would be left unchanged.
| Exit code | Meaning |
|---|---|
0 | No changes needed (or files reformatted successfully in write mode) |
1 | At least one file would be / was reformatted |
123 | Internal error — syntax error in source, malformed config, etc. |
CI gates the build on 0. The distinction between 1 and 123 matters when wrapping Black in scripts — treat 123 as a hard failure but 1 as "user must reformat".
The magic trailing comma
Black treats a trailing comma in any collection or call as an instruction to split the element across lines, one per line. If the comma is present, Black splits even when the expression would fit on one line. If absent, Black collapses to one line when it fits and only splits when forced. This is the only style decision Black exposes in the source itself, and the most idiomatic way to keep diff-friendly code: add a trailing comma to anything you expect to grow.
# No trailing comma — Black collapses to one line if it fits
items = [1, 2, 3]
# Trailing comma — Black keeps each element on its own line, always
items = [
1,
2,
3,
]
# Function calls work the same way
client.upload(
bucket="logs",
key="2026/05/25.json",
body=payload,
metadata={"source": "api"},
)
The downstream benefit: appending a fifth element to the list above is a one-line diff (+ 4,), not a four-line re-layout. Black's skip-magic-trailing-comma = true disables this — opt out only if your codebase explicitly doesn't want this behaviour.
String normalization
By default Black rewrites single quotes to double quotes. The PEP 8 stance is that quotes are interchangeable and consistency matters; Black picks one and enforces it. Set skip-string-normalization = true to disable, but the resulting "Black, but with single quotes" config is widely considered an anti-pattern — the first run on existing code produces a large diff and you've now created a non-standard variant of Black.
# Before
x = 'hello'
y = "world"
z = 'he said "hi"'
# After
x = "hello"
y = "world"
z = 'he said "hi"' # left alone — switching would require escaping
Strings containing a literal double-quote are left as-is to avoid having to escape inside. f-strings, raw strings, and byte strings are all normalised the same way.
Per-block escape: fmt: off / fmt: on / fmt: skip
For hand-aligned tables, magic-number arrays, and complex multi-line expressions, Black exposes two opt-out markers. Use sparingly — every fmt: off block is a place where Black's invariants no longer hold and the next contributor has to think harder. # fmt: skip is the line-level form and is preferred for single-line cases.
# fmt: off
LOOKUP = {
0x00: "NUL", 0x01: "SOH", 0x02: "STX", 0x03: "ETX",
0x04: "EOT", 0x05: "ENQ", 0x06: "ACK", 0x07: "BEL",
0x08: "BS", 0x09: "HT", 0x0A: "LF", 0x0B: "VT",
}
# fmt: on
# Or on a single line
matrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] # fmt: skip
Output: (none — code unchanged by Black between markers)
include, exclude, force-exclude — file matching
Black matches files in three layers. include is a regex of files to consider — by default \.pyi?$ (Python and stub files). exclude is a regex of files to skip from that set; Black has a sensible default (.venv, .git, dist, build, __pycache__, etc.). extend-exclude adds to that default. force-exclude is the strongest — it overrides files explicitly listed on the command line, so even black generated/foo.py will skip the file.
[tool.black]
include = '\.pyi?$'
extend-exclude = '''
/(
migrations/
| generated/
| _drafts/
| vendored/
)/
'''
force-exclude = '^/build/'
# Show which files Black would touch without modifying anything
black --check --verbose .
Output:
Identified `.../src/api.py` as 'simple_file' — formatting
Identified `.../tests/test_api.py` as 'simple_file' — formatting
Identified `.../migrations/0001_initial.py` as 'gitignored' — skipping
2 files would be left unchanged.
1 file would be reformatted.
Editor integration
Every modern Python-capable editor has a Black plugin. The pattern is the same: format on save, format on paste optional. VS Code's official Black extension (ms-python.black-formatter) and the Ruff extension both call Black-compatible formatters; below is the VS Code Black setup.
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true,
"editor.formatOnPaste": false
},
"black-formatter.args": ["--line-length=100"]
}
| Editor | Plugin / approach |
|---|---|
| VS Code | ms-python.black-formatter (or use Ruff extension's Black-compat formatter) |
| PyCharm | Settings → Tools → Black; enable "On save" and "On reformat" |
| Neovim | conform.nvim with the black formatter, or null-ls |
| Sublime Text | sublack plugin |
| Emacs | python-black package or format-all |
| Vim | psf/black.vim |
The editor reads pyproject.toml automatically, so --line-length and other settings don't need to be repeated per-editor.
Workflow: isort + Black
Black formats but doesn't sort imports. Pre-Ruff, the canonical pairing was isort (sort and group) followed by black (format). isort needed to be configured Black-compatible (profile = "black") so the two didn't fight each other over multi-line import layouts.
[tool.isort]
profile = "black"
line_length = 100
combine_as_imports = true
known_first_party = ["myapp"]
# Canonical pre-Ruff pipeline
isort src/
black src/
Output: (none — exits 0 on success)
This combination is still in use in many older codebases; new projects skip both in favour of Ruff (ruff check --select I --fix && ruff format).
black vs ruff format
ruff format is byte-for-byte compatible with Black for ~99.9% of inputs — Astral runs a continuous compatibility test against a large corpus and tracks deliberate divergences in a public document. The differences are:
| Aspect | Black | ruff format |
|---|---|---|
| Speed | ~1× | 10–30× faster |
| Configuration | [tool.black] | [tool.ruff.format] |
| Combined with linting | Separate from flake8/isort | Same binary as ruff check |
| String normalisation | Always to double quotes | quote-style = "double" | "single" | "preserve" |
| Magic trailing comma | Honoured | Honoured (same semantics) |
| Docstring code | Reformatted if requested (via plugins) | docstring-code-format = true built-in |
| Preview mode | preview = true | preview = true |
For a new project, prefer ruff format. For an established project that already uses Black with pre-commit, CI, and editor configs, the migration cost is rarely worth it — both produce the same output. The decision points are:
- Speed matters (CI > 30s on lint) → migrate to ruff.
- Already on ruff for linting → switch to
ruff formatto drop a dependency. - Need
docstring-code-format→ both support it. - Need single-quote output → only ruff supports it natively.
Plugins and ecosystem
Black resists most plugins by design — extending the formatter is "extending the style" and the project pushes back against that. The two adjacent tools that gained traction are:
blackdoc— runs Black on Python code embedded in Markdown and reStructuredText files (e.g. README examples). Pre-commit hook id:blackdoc.docformatter— separate tool that reformats docstring text (not code inside docstrings); pairs with Black so each owns its half of the file.
pip install blackdoc docformatter
blackdoc README.md
docformatter --in-place --wrap-summaries 88 --wrap-descriptions 88 src/
Output:
Reformatted README.md
Caching
Black caches per-file results in ~/.cache/black/<version>/cache.pickle keyed by file path, content hash, and Black version. Re-running over unchanged files is near-instant. --no-cache bypasses the cache; deleting the directory invalidates it. CI workflows can persist the cache across runs but rarely need to — Black's cold-start is already sub-second on small repos.
# Cold run
time black --check src/
# Cached re-run
time black --check src/
# Bypass cache
black --no-cache --check src/
# Wipe the cache
rm -rf ~/.cache/black
Output:
All done! ✨ 🍰 ✨
47 files would be left unchanged.
real 0m0.124s
user 0m0.098s
sys 0m0.026s
Stdin and file-less invocation
black - reads from stdin and writes formatted code to stdout. Useful for xargs, editor integrations that pipe selections, and one-off snippets. Use --stdin-filename to tell Black what file the input represents (so per-file config picks up).
echo "x=1; y=2" | black -
echo "x=1" | black --stdin-filename src/api.py -
Output:
x = 1
y = 2
Common pitfalls
black .accidentally formats migrations or vendored code — the defaultexcludeonly knows about VCS and build dirs. Addextend-excludeformigrations/,vendor/,generated/.excludevsextend-exclude—excludereplaces the default;extend-excludeadds. Set the wrong one and Black starts reformatting.venv/ornode_modules/. Almost always useextend-exclude.--line-lengthmismatch between editor and CI — editor uses default 88, CI uses 100, every save produces churn. Pin the value inpyproject.tomlso both pick it up automatically.# fmt: offblocks leak when edited — once a block is marked off-limits, future edits inside it don't get formatted. Keep these blocks short and contained.target-versionnot set — Black defaults to the version it was installed under, which produces inconsistent diffs across team members on different Pythons. Pin it.--checkexit code 1 confused with a crash — exit 1 is "would reformat", not "broke". Treat as expected in CI. Hard failures use exit 123.- Magic trailing comma turned off accidentally —
skip-magic-trailing-comma = trueproduces noisier diffs over time as collections grow. Leave it off unless you have a specific reason. skip-string-normalizationfor "personal preference" — creates a non-standard Black variant that future contributors won't expect. Either accept double quotes or useruff formatwithquote-style = "single".- Black version drift between local and CI — pin Black in
pyproject.toml(under[project.optional-dependencies]dev) sopip install -e ".[dev]"matches CI exactly. Major version bumps occasionally rewrite code. - Running Black before isort — isort then re-orders imports, Black re-flows them, producing a churning two-pass diff. Always: isort first, Black second (or use Ruff to do both atomically).
Real-world recipes
Bootstrap a Python project with Black
Three commands and one config block. Locks the Black version, applies it to the source tree, and wires pre-commit so it runs on every commit going forward.
cd ~/code/myproject
pip install black==24.10.0
cat >> pyproject.toml <<'EOF'
[tool.black]
line-length = 100
target-version = ["py312"]
extend-exclude = '/(\.git|\.venv|dist|build|migrations)/'
EOF
black src/ tests/
git add -A && git commit -m "style: apply black formatting"
Output: (none — exits 0 on success)
Migrate an unformatted codebase
The first Black run on an old project produces a large diff — that's expected. Land it as one commit so future blame ignores it, then enforce going forward.
# Step 1: one bulk commit
black src/ tests/
git add -A
git commit -m "style: apply black formatting across repo"
# Step 2: train git blame to ignore the bulk commit
cat >> .git-blame-ignore-revs <<EOF
$(git rev-parse HEAD)
EOF
git config blame.ignoreRevsFile .git-blame-ignore-revs
git add .git-blame-ignore-revs
git commit -m "chore: ignore black bulk-format in git blame"
# Step 3: wire pre-commit to enforce going forward
Output:
[main 1a2b3c4] style: apply black formatting across repo
89 files changed, 482 insertions(+), 415 deletions(-)
The
.git-blame-ignore-revsfile plusblame.ignoreRevsFileis essential — without it, every line in the repo appears to have been last changed by the formatting commit, makinggit blameuseless.
CI: fail build if Black is needed
Minimal GitHub Actions workflow. Pinning the Black version is the critical line — it guarantees CI sees the same formatting decisions as developers.
# .github/workflows/format.yml
name: format
on: [push, pull_request]
jobs:
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install black==24.10.0
- run: black --check --diff src/ tests/
Output (on failure):
--- src/api.py 2026-05-25 09:00:00
+++ src/api.py 2026-05-25 09:00:00
@@ -1,2 +1,4 @@
-def foo(x,y):return x+y
+def foo(x, y):
+ return x + y
+
would reformat src/api.py
Oh no! 💥 💔 💥
1 file would be reformatted, 47 files would be left unchanged.
Error: Process completed with exit code 1.
Pre-commit with auto-fix on commit
Combine Black with isort for the canonical pre-ruff style pipeline. Both hooks pin their versions; autoupdate walks them forward weekly.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
args: ["--profile=black", "--line-length=100"]
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
args: ["--line-length=100"]
pre-commit install
pre-commit run --all-files
Output:
isort....................................................................Passed
black....................................................................Passed
isort must come before Black. isort decides import grouping, Black then formats the imports' layout. Reversing the order produces a double-pass diff where Black formats, isort re-orders, Black re-formats, and the commit fails twice in a row.
Format only changed files (fast loop)
Reformatting the whole tree before a small PR is wasted work. Use git diff to identify changed .py files and pipe to Black.
#!/usr/bin/env bash
# scripts/black-changed.sh
files=$(git diff --name-only --diff-filter=ACMR origin/main...HEAD -- '*.py')
[ -z "$files" ] && { echo "no Python changes"; exit 0; }
echo "$files" | xargs black --check --diff
Output: (none — exits 0 on success)
bash scripts/black-changed.sh
Output:
All done! ✨ 🍰 ✨
3 files would be left unchanged.
Switch from Black to ruff format
For projects already on Black + isort + flake8 + pyupgrade, the migration to Ruff replaces all four. The Black output is byte-compatible with ruff format for almost all inputs, so the formatting diff at the changeover should be tiny.
# 1. Lock in the current Black baseline
black src/ tests/
isort src/ tests/
git add -A && git commit -m "chore: final pass of black + isort"
# 2. Install ruff, drop the others
pip uninstall -y black isort flake8 pyupgrade
pip install ruff==0.6.9
# 3. Add [tool.ruff] config to pyproject.toml
cat >> pyproject.toml <<'EOF'
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
extend-select = ["F", "E", "W", "I", "B", "UP"]
[tool.ruff.format]
quote-style = "double"
EOF
# 4. Run ruff over the codebase
ruff check --fix src/ tests/
ruff format src/ tests/
git add -A && git commit -m "chore: replace black/isort/flake8 with ruff"
Output:
Found 0 errors.
89 files left unchanged
Skip Black for one file or block
Some legacy code (auto-generated bindings, hand-aligned constant tables, ML model definitions) is genuinely worse after Black. The two escape hatches:
# Per-file: force-exclude in pyproject.toml
# [tool.black]
# force-exclude = '^/src/generated/'
# Per-block: bracket with fmt: off / fmt: on
# fmt: off
CONSTANTS = [
("Q1", 100, 9.5, "active"),
("Q2", 150, 10.2, "active"),
("Q3", 200, 11.0, "frozen"),
]
# fmt: on
# Per-line: trailing # fmt: skip
result = some_function(arg1, arg2, arg3, arg4, arg5) # fmt: skip
Format Python in Markdown / docs
blackdoc extends Black to code blocks inside Markdown and RST files. Use it on README.md, docs/, and tutorials so README examples stay formatted in lock-step with the source tree.
pip install blackdoc
# Format Python blocks in Markdown
blackdoc README.md docs/
# Pre-commit hook
cat >> .pre-commit-config.yaml <<'EOF'
- repo: https://github.com/keewis/blackdoc
rev: v0.3.9
hooks:
- id: blackdoc
EOF
Output:
Reformatted 2 files
See also
sections/python/ruff— modern alternative;ruff formatis Black-compatible.sections/python/pre-commit— the canonical place to wire Black into git.sections/python/mypy— pairs with Black on the static-analysis side of the toolchain.sections/python/pyproject-toml— the file where[tool.black]lives.