cheat sheet

black

Package-level reference for black on PyPI — install variants, version policy, the [d] and [jupyter] extras, and how it relates to ruff format.

black

What it is

black is an opinionated, deterministic Python code formatter created by Łukasz Langa and now stewarded by the Python Software Foundation. The selling point is the absence of knobs: black picks a single canonical style and applies it, so style debate disappears from code review.

In 2026 the formatter market is split — many established projects still standardise on black, while newer projects increasingly pick ruff format (a near-drop-in Rust reimplementation). Black remains the reference implementation that everyone else compares against.

Install

bash
pip install black

Output: (none — exits 0 on success)

bash
uv add --dev black

Output: dependency added to the dev group in pyproject.toml

bash
poetry add --group dev black

Output: updated lockfile + dev install

bash
pipx install black

Output: installed to isolated venv, black CLI on PATH (recommended for global use)

Versioning & Python support

  • black uses a calendar-versioning-ish scheme: YY.MM.x (e.g. 24.10.0). There's no semver — every release is "stable" but the formatting output may shift between years.
  • --target-version lets you pin the output style to a Python version (py38..py312), independent of the interpreter running black.
  • Recent black releases run on Python 3.8+; Python 3.7 was dropped in 23.1.0 (early 2023).
  • The project pins style decisions for a calendar year — running an old black on a new codebase produces stable diffs, but every January release may re-flow some constructs.

Package metadata

  • Maintainer: Python Software Foundation / psf org on GitHub
  • Project home: github.com/psf/black
  • Docs: black.readthedocs.io
  • PyPI: pypi.org/project/black
  • License: MIT
  • Governance: PSF-stewarded, with a small core-maintainer team
  • First released: 2018
  • Downloads: tens of millions per month

Optional dependencies & extras

ExtraAdds
black[d]aiohttp and the blackd daemon binary — long-running process so editor integrations skip startup cost per save.
black[jupyter]tokenize-rt + Jupyter support so black notebook.ipynb formats cells in place.
black[uvloop]Faster event loop for blackd on Linux/macOS.
black[colorama]Colour output on Windows terminals.

Combine extras with comma syntax: pip install "black[d,jupyter]".

Core deps (always installed): click, mypy-extensions, packaging, pathspec, platformdirs, tomli (on Python < 3.11).

Alternatives

PackageTrade-off
ruff format~30× faster, near-identical style. Single binary that also lints. Default choice for new projects.
autopep8PEP 8 only — fixes whitespace/indentation but not line breaks or quote style. Conservative; preserves more of your code.
yapf (Google)Configurable style. Use when you need to match a non-black house style.
isortSorts imports only. Pair with black; ruff replaces both.
darkerFormats only diff hunks, not whole files. Useful for incremental adoption on legacy codebases.

Common gotchas

  1. Almost no knobs by design. The only real settings are --line-length (default 88) and --target-version. Don't expect to tweak quote style, trailing commas, or blank-line rules — the answer is always "no".
  2. Line length 88, not 79. Black deviates from PEP 8's 79-character recommendation to reduce noisy re-wraps. Teams sometimes set --line-length 100 for wider monitors; doing so is supported but reduces black's "everyone agrees" benefit.
  3. Magic trailing comma forces multi-line. If a collection literal has a trailing comma after the last element, black keeps it across multiple lines even if it would fit on one. Drop the comma to let black collapse it.
  4. Black and ruff-format are very close but not byte-identical. Edge cases around string quoting, parenthesisation of complex expressions, and chained method calls can diverge. Don't run both in CI on the same files — pick one.
  5. blackd cache lives in ~/.cache/black/. When upgrading black versions, stale cache occasionally causes "no changes" output for a file that should reformat. Delete the cache dir to force a clean re-run.
  6. Notebook support requires black[jupyter]. Plain pip install black does not include .ipynb handling — the error message is "ImportError: Install nbformat" which obscures the real fix.
  7. The yearly "Stable Style" promise has limits. Major formatting changes are gated behind --preview, but the non-preview style still drifts slowly. Pin black in CI (black==24.10.0) to avoid surprise diffs on Monday morning.

Configuration & layout patterns

Black has fewer knobs than any other Python formatter — but the few that exist live in pyproject.toml. The full surface:

toml
[tool.black]
line-length = 88
target-version = ["py310", "py311", "py312"]
include = '\.pyi?$'
extend-exclude = '''
/(
  | migrations
  | \.venv
)/
'''
preview = false
unstable = false
required-version = "24"
skip-string-normalization = false
skip-magic-trailing-comma = false

include/extend-exclude accept verbose regex strings (triple-quoted, multi-line). force-exclude overrides anything passed on the command line — useful when a CI invocation accidentally targets generated code. required-version causes black to refuse to run if the installed major doesn't match — a hard guard against unintended style drift across contributors.

Layout strategies:

  • Single config, single style — most repos. pyproject.toml at the root, every contributor runs the same black version (pinned in dev deps + pre-commit).
  • Monorepo with per-package overrides — each sub-project has its own pyproject.toml. Black walks up from each file to find the nearest config, so per-package line-length works without extra plumbing. Beware: the root config wins for any file outside a sub-project boundary, which can be surprising.
  • Vendored / third-party code — keep it under extend-exclude so black never touches it. Migrations, generated protobuf stubs, and copied-from-upstream code all qualify.
  • Gradual adoption — pair black with darker to format only diff hunks. The repo's "blackened" surface grows commit by commit instead of in one giant diff.

black --target-version py312 pins the output style to Python 3.12 even if the runner is older. This decouples the formatter version from the runtime — useful for libraries that publish wheels targeting multiple Pythons.

Real-world recipes

Monorepo with mixed line lengths

toml
# packages/api/pyproject.toml
[tool.black]
line-length = 100
target-version = ["py312"]

# packages/lib/pyproject.toml
[tool.black]
line-length = 88
target-version = ["py310", "py311", "py312"]

Black resolves config per file based on the nearest ancestor pyproject.toml with a [tool.black] table. The API service can run wider (more lines fit on modern monitors); the library stays at the canonical 88 to match downstream expectations.

Gradual adoption on a legacy codebase

bash
pip install darker
darker --revision main src/

Output: runs black only on hunks that differ from main. A 200,000-line codebase migrates in PR-sized chunks rather than one mega-diff that destroys git blame.

After a few months, when most files have been touched, flip to full black:

bash
black src/
git commit -am "style: format remaining files with black"
git config --local blame.ignoreRevsFile .git-blame-ignore-revs
echo "<sha>" >> .git-blame-ignore-revs

Output: every Python file under src/ reformatted; the commit SHA is recorded in .git-blame-ignore-revs so git blame skips it. The GitHub blame UI honours the same file automatically.

.git-blame-ignore-revs (and the GitHub equivalent) preserves blame across the formatting commit — without it, every line points at the formatter commit.

Per-block escape hatches

python
# fmt: off
DATA = [
    ["a",  1,  2,  3],
    ["bb", 11, 22, 33],
    ["ccc",111,222,333],
]
# fmt: on

Black respects # fmt: off / # fmt: on blocks and the single-line # fmt: skip marker. Use these for hand-aligned matrices, ASCII art, or code where the wrapping decision genuinely matters. Don't sprinkle them — once you've got a dozen fmt: off blocks the "one canonical style" benefit is gone.

blackd for editor integration

bash
pip install "black[d]"
blackd --bind-host 127.0.0.1 --bind-port 45484

Output: runs the formatter as a long-lived HTTP service. Editors (black-macchiato, VS Code's Black extension) POST file contents and receive formatted output, skipping the ~200 ms Python startup per save. On a 4,000-file project this is the difference between save-feels-instant and save-feels-laggy.

Performance tuning

Black is single-threaded per file but parallel across files. Most slowness comes from elsewhere:

  • Parallel by default. Black uses a process pool sized to the CPU count. black --workers N overrides for cases where the I/O subsystem is the bottleneck (network filesystems, slow SSDs).
  • blackd for IDE integration. Skips the Python startup cost on every save. The daemon binds to a port and accepts file contents via HTTP.
  • --fast skips AST safety checks. Saves ~20 % on large files. Pair with --check in CI to catch any AST-corruption regressions; never run --fast on first-time formatting.
  • Cache lives in platformdirs.user_cache_dir() / "black". Black hashes file content + black version + config; unchanged files skip the formatter entirely. CI runners benefit from caching this directory.
  • Don't run black recursively on node_modules/.venv. The default excludes catch these, but a stray black . in a monorepo without a pyproject.toml walks every directory. Set extend-exclude defensively.

For comparison, ruff format is ~30× faster on identical input. The reason to stay on black is style stability and tooling integration; the reason to switch is build-pipeline speed.

Version migration guide

Black uses calendar versioning (YY.MM.x). The major formatting decisions ship in the January release of each year, gated behind --preview for the prior 12 months.

24.x (2024)

  • Hug-parens-with-magic-trailing-comma now extends to subscripts (d[key,] formats differently).
  • match statement formatting refined; case-pattern wrapping changed for long subject expressions.
  • --unstable flag introduced as a tier below --preview for in-flux features that may regress between releases.

23.x (2023)

  • Python 3.7 dropped. Minimum interpreter is now 3.8.
  • Trailing comma handling unified across function definitions, calls, and collections.

22.x (2022)

  • --target-version py37/py38/py39 started influencing output (e.g. f-string formatting on supporting versions).

Upgrade strategy:

  1. Pin exact version in pyproject.toml dev deps and in .pre-commit-config.yaml rev:.
  2. Bump both pins in a dedicated style PR. Expect a diff — black's "stable style" promise is gated changes, not zero changes.
  3. Add the bump-commit SHA to .git-blame-ignore-revs to preserve blame.
  4. Use black --check --preview in a follow-up PR to preview next year's changes before they're default.

The required-version config key (since black 22.x) enforces version match at runtime — black exits with an error if the installed version doesn't match. Effective belt-and-braces guard against contributors running mismatched versions.

Troubleshooting common errors

SymptomCauseFix
error: cannot format X: Cannot parse: ...Syntax error in the file (or a Python version mismatch — file uses match but --target-version py39)Either fix the syntax or bump target-version to a Python that supports it.
would reformat X from --check despite no visible diffTrailing whitespace or BOMRun black X to see the actual changes; check editor settings for trailing-whitespace stripping.
Oh no! 💥 💔 💥 final-line ASCIIInternal black panic (AST safety check failed)Rare; usually a black bug. Try --fast to skip the safety check; file an issue with the input.
Black format diverges between two contributorsDifferent black versions installed locallyPin exact version in dev deps; add required-version to pyproject.toml.
ImportError: Install nbformat when formatting notebooksMissing [jupyter] extrapip install "black[jupyter]".
Black ignores a file with # fmt: skipThe marker is multi-line — only the line annotated is skippedUse # fmt: off / # fmt: on for multi-line escapes.
Black formats inside # fmt: off block on upgradeThe block boundary became ambiguous with the new styleMove the # fmt: off to immediately precede the first statement, not after a blank line.

The --diff flag shows exactly what black would change without writing — invaluable for CI failure diagnosis and for one-off audits of legacy code before adoption.

Ecosystem integrations

Black composes well with the rest of the Python toolchain because it has no overlap with most tools:

  • isort — sorts imports; runs before black so black gets clean trailing commas. Use isort --profile black to align settings.
  • ruff format — drop-in alternative. Don't run both. Pick one per repo; ruff is the lower-overhead choice in 2026 unless you have a strong reason for black.
  • ruff check — orthogonal: lint rules, not formatting. Run after black/ruff format.
  • mypy — orthogonal. Black doesn't change types.
  • pre-commitpsf/black-pre-commit-mirror is the canonical hook source. Pin to an exact version matching the dev dep.
  • darker — diff-only black for gradual adoption.
  • Editor LSPs — VS Code's Black extension, PyCharm's "Black formatter" setting, Neovim's null-ls. All invoke either black or blackd.
  • GitHub Action psf/black@stable — runs black against the PR and posts a check. Avoid; pin a specific version via your own workflow instead so the style doesn't drift mid-PR.

Plugin & rule ecosystem

Black explicitly has no plugin system — by design. The only extension mechanisms:

  • --preview — opt into next-year's formatting changes early. Use to validate that you'll accept the changes before they become default.
  • --unstable — even more experimental; features may regress between releases. Avoid in production.
  • Editor integrations — VS Code's Black extension, PyCharm's built-in Black formatter, Neovim's null-ls, Emacs blacken-mode. All invoke either black directly or blackd.
  • pre-commit mirrorpsf/black-pre-commit-mirror exposes Black as a pre-commit hook. The mirror exists because PSF's release tags aren't pre-commit-compatible directly.
  • darker — third-party wrapper that runs Black on diff hunks only. Useful for gradual adoption on legacy codebases.

The "no plugins" stance is deliberate — Black's value proposition is one canonical style across every project. Plugins would fragment that. If you need different behaviour, the answer is "use a different tool".

Testing strategies

Black itself is well-tested upstream. For your project, the relevant tests are CI gates rather than unit tests:

  • black --check in CI catches drift. Fail the build if any file would be reformatted.
  • black --check --preview in a separate non-blocking CI job — surfaces what next year's style will demand before it's mandatory.
  • AST-equivalence verification — Black guarantees the AST is unchanged after formatting. The --safe flag (default) enforces this; --fast skips for speed.
  • Idempotency testblack file.py && black --check file.py should always pass on the second run. If it doesn't, you've found a black bug; report upstream.

For a library that ships its own formatter integration (rare), test against a corpus of input files and verify byte-equality of output across versions. Black publishes a stability promise per calendar year; the corpus catches breakage of that promise.

CI integration

yaml
name: lint
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"
      - run: pip install "black==24.10.0"
      - run: black --check --diff src/ tests/

Key choices:

  • Exact pinblack==24.10.0, never ~=24.10 or latest. Style drift across PRs is the primary CI failure mode for black.
  • --check --diff — exits non-zero with a diff if any file would change. Posting the diff in the failure message lets contributors copy-paste the fix.
  • Run on a single Python version — black's output is identical regardless of the runner Python (modulo --target-version). No need for a matrix.
  • Cache ~/.cache/blackactions/cache@v4 with key black-${{ hashFiles('pyproject.toml') }} cuts cold-CI time noticeably on large codebases.

For monorepos, scope black to the changed paths:

yaml
- uses: dorny/paths-filter@v3
  id: changes
  with:
    filters: |
      python: ['**/*.py', '**/*.pyi']
- run: black --check --diff $(git diff --name-only origin/main -- '*.py')
  if: steps.changes.outputs.python == 'true'

This skips the format check on PRs that don't touch Python — a small win that compounds across a busy monorepo.

When NOT to use this

Black's reach is wide but not universal:

  • Tabs-mandated repos. Black is spaces-only; no flag converts. If house style mandates tabs (rare, but exists in some legacy codebases), use autopep8 or hand-config yapf.
  • Strong house style that disagrees with black on quotes, line length, or trailing commas. Black gives you --line-length, --skip-string-normalization, and --skip-magic-trailing-comma — beyond that, switch tools.
  • Notebook-first projects where you don't want cell-by-cell formatting drift. black[jupyter] works, but the diffs in .ipynb files are noisy regardless of formatter.
  • Performance-critical CI where the 30× speed gap with ruff format matters. On a 100k-line repo, black takes ~5 s, ruff takes ~0.2 s. Both are negligible; the gap matters at 1M+ lines.
  • Already-on-ruff-format projects. Switching back from ruff to black is a one-time diff for marginal gain.

See also