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
pip install black
Output: (none — exits 0 on success)
uv add --dev black
Output: dependency added to the dev group in pyproject.toml
poetry add --group dev black
Output: updated lockfile + dev install
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-versionlets 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 /
psforg 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
| Extra | Adds |
|---|---|
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
| Package | Trade-off |
|---|---|
ruff format | ~30× faster, near-identical style. Single binary that also lints. Default choice for new projects. |
autopep8 | PEP 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. |
isort | Sorts imports only. Pair with black; ruff replaces both. |
darker | Formats only diff hunks, not whole files. Useful for incremental adoption on legacy codebases. |
Common gotchas
- 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". - Line length 88, not 79. Black deviates from PEP 8's 79-character recommendation to reduce noisy re-wraps. Teams sometimes set
--line-length 100for wider monitors; doing so is supported but reduces black's "everyone agrees" benefit. - 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.
- 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.
blackdcache 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.- Notebook support requires
black[jupyter]. Plainpip install blackdoes not include.ipynbhandling — the error message is "ImportError: Install nbformat" which obscures the real fix. - 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:
[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.tomlat 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-packageline-lengthworks 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-excludeso black never touches it. Migrations, generated protobuf stubs, and copied-from-upstream code all qualify. - Gradual adoption — pair black with
darkerto 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
# 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
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:
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
# 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
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 Noverrides for cases where the I/O subsystem is the bottleneck (network filesystems, slow SSDs). blackdfor IDE integration. Skips the Python startup cost on every save. The daemon binds to a port and accepts file contents via HTTP.--fastskips AST safety checks. Saves ~20 % on large files. Pair with--checkin CI to catch any AST-corruption regressions; never run--faston 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 strayblack .in a monorepo without apyproject.tomlwalks every directory. Setextend-excludedefensively.
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). matchstatement formatting refined; case-pattern wrapping changed for long subject expressions.--unstableflag introduced as a tier below--previewfor 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/py39started influencing output (e.g.f-stringformatting on supporting versions).
Upgrade strategy:
- Pin exact version in
pyproject.tomldev deps and in.pre-commit-config.yamlrev:. - Bump both pins in a dedicated style PR. Expect a diff — black's "stable style" promise is gated changes, not zero changes.
- Add the bump-commit SHA to
.git-blame-ignore-revsto preserve blame. - Use
black --check --previewin 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
| Symptom | Cause | Fix |
|---|---|---|
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 diff | Trailing whitespace or BOM | Run black X to see the actual changes; check editor settings for trailing-whitespace stripping. |
Oh no! 💥 💔 💥 final-line ASCII | Internal 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 contributors | Different black versions installed locally | Pin exact version in dev deps; add required-version to pyproject.toml. |
ImportError: Install nbformat when formatting notebooks | Missing [jupyter] extra | pip install "black[jupyter]". |
Black ignores a file with # fmt: skip | The marker is multi-line — only the line annotated is skipped | Use # fmt: off / # fmt: on for multi-line escapes. |
Black formats inside # fmt: off block on upgrade | The block boundary became ambiguous with the new style | Move 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. Useisort --profile blackto 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-commit—psf/black-pre-commit-mirroris 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 eitherblackorblackd. - 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, Emacsblacken-mode. All invoke eitherblackdirectly orblackd. pre-commitmirror —psf/black-pre-commit-mirrorexposes Black as a pre-commit hook. The mirror exists because PSF's release tags aren'tpre-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 --checkin CI catches drift. Fail the build if any file would be reformatted.black --check --previewin 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
--safeflag (default) enforces this;--fastskips for speed. - Idempotency test —
black file.py && black --check file.pyshould 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
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 pin —
black==24.10.0, never~=24.10orlatest. 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/black—actions/cache@v4with keyblack-${{ hashFiles('pyproject.toml') }}cuts cold-CI time noticeably on large codebases.
For monorepos, scope black to the changed paths:
- 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
autopep8or hand-configyapf. - 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.ipynbfiles are noisy regardless of formatter. - Performance-critical CI where the 30× speed gap with
ruff formatmatters. 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
- Python: black — configuration, editor integration, recipes
- Packages: pip-ruff — the faster, multipurpose alternative
- Packages: pip-pre-commit — run black on every commit