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

bash
pip install black

Output: (none — exits 0 on success)

Quick example

python
# messy.py (before black)
x = {'a':1,'b':2}
def foo(x,y,z):
  return x+y+z
result=foo(1,2,3)
bash
black messy.py

Output:

text
reformatted messy.py
All done! ✨ 🍰 ✨
1 file reformatted.

After black:

python
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.

--check vs no flagblack --check reports whether files would change but does not change them (exit code 1 if any file would be reformatted). Running black without --check modifies files in place.

Use # fmt: off and # fmt: on comments to disable black for specific blocks (e.g. hand-aligned tables or magic number arrays). Use sparingly.

Richer example — check mode in CI

bash
# CI: fail if code is not already black-formatted
black --check --diff src/

Output (when files need formatting):

text
--- 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):

text
All done! ✨ 🍰 ✨
5 files would be left unchanged.

pyproject.toml configuration

Black has intentionally minimal config. The only common options:

toml
[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

EditorPlugin
VS CodePython extension → set "editor.defaultFormatter": "ms-python.black-formatter"
PyCharmSettings → Tools → Black
Neovimconform.nvim or null-ls
Emacsblacken-mode

Pre-commit hook

yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black
bash
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:

  1. Determinism — formatting is a pure function of source code; CI can re-format on any machine and get the same bytes.
  2. 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.
  3. Limited configurationline-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-normalization is 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.

toml
[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-exclude adds to Black's default exclusions; exclude replaces them. Almost always use extend-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.

bash
# 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):

text
--- 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 codeMeaning
0No changes needed (or files reformatted successfully in write mode)
1At least one file would be / was reformatted
123Internal 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.

python
# 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.

python
# 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.

python
# 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.

toml
[tool.black]
include = '\.pyi?$'
extend-exclude = '''
/(
    migrations/
  | generated/
  | _drafts/
  | vendored/
)/
'''
force-exclude = '^/build/'
bash
# Show which files Black would touch without modifying anything
black --check --verbose .

Output:

text
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.

json
{
  "[python]": {
    "editor.defaultFormatter": "ms-python.black-formatter",
    "editor.formatOnSave": true,
    "editor.formatOnPaste": false
  },
  "black-formatter.args": ["--line-length=100"]
}
EditorPlugin / approach
VS Codems-python.black-formatter (or use Ruff extension's Black-compat formatter)
PyCharmSettings → Tools → Black; enable "On save" and "On reformat"
Neovimconform.nvim with the black formatter, or null-ls
Sublime Textsublack plugin
Emacspython-black package or format-all
Vimpsf/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.

toml
[tool.isort]
profile = "black"
line_length = 100
combine_as_imports = true
known_first_party = ["myapp"]
bash
# 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:

AspectBlackruff format
Speed~1×10–30× faster
Configuration[tool.black][tool.ruff.format]
Combined with lintingSeparate from flake8/isortSame binary as ruff check
String normalisationAlways to double quotesquote-style = "double" | "single" | "preserve"
Magic trailing commaHonouredHonoured (same semantics)
Docstring codeReformatted if requested (via plugins)docstring-code-format = true built-in
Preview modepreview = truepreview = 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 format to 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.
bash
pip install blackdoc docformatter
blackdoc README.md
docformatter --in-place --wrap-summaries 88 --wrap-descriptions 88 src/

Output:

text
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.

bash
# 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:

text
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).

bash
echo "x=1; y=2" | black -
echo "x=1" | black --stdin-filename src/api.py -

Output:

text
x = 1
y = 2

Common pitfalls

  1. black . accidentally formats migrations or vendored code — the default exclude only knows about VCS and build dirs. Add extend-exclude for migrations/, vendor/, generated/.
  2. exclude vs extend-excludeexclude replaces the default; extend-exclude adds. Set the wrong one and Black starts reformatting .venv/ or node_modules/. Almost always use extend-exclude.
  3. --line-length mismatch between editor and CI — editor uses default 88, CI uses 100, every save produces churn. Pin the value in pyproject.toml so both pick it up automatically.
  4. # fmt: off blocks leak when edited — once a block is marked off-limits, future edits inside it don't get formatted. Keep these blocks short and contained.
  5. target-version not set — Black defaults to the version it was installed under, which produces inconsistent diffs across team members on different Pythons. Pin it.
  6. --check exit code 1 confused with a crash — exit 1 is "would reformat", not "broke". Treat as expected in CI. Hard failures use exit 123.
  7. Magic trailing comma turned off accidentallyskip-magic-trailing-comma = true produces noisier diffs over time as collections grow. Leave it off unless you have a specific reason.
  8. skip-string-normalization for "personal preference" — creates a non-standard Black variant that future contributors won't expect. Either accept double quotes or use ruff format with quote-style = "single".
  9. Black version drift between local and CI — pin Black in pyproject.toml (under [project.optional-dependencies] dev) so pip install -e ".[dev]" matches CI exactly. Major version bumps occasionally rewrite code.
  10. 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.

bash
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.

bash
# 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:

text
[main 1a2b3c4] style: apply black formatting across repo
 89 files changed, 482 insertions(+), 415 deletions(-)

The .git-blame-ignore-revs file plus blame.ignoreRevsFile is essential — without it, every line in the repo appears to have been last changed by the formatting commit, making git blame useless.

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.

yaml
# .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):

text
--- 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.

yaml
# .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"]
bash
pre-commit install
pre-commit run --all-files

Output:

text
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.

bash
#!/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
bash scripts/black-changed.sh

Output:

text
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.

bash
# 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:

text
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:

python
# 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.

bash
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:

text
Reformatted 2 files

See also

  • sections/python/ruff — modern alternative; ruff format is 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.