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