Black / Ruff Format
Automatic formatters eliminate style debates by enforcing one deterministic output. Ruff format (Black-compatible) is the recommended default; Black remains widely used in existing projects.
Recipe
# Ruff format (recommended)
uv run ruff format .
# Black (legacy projects)
uv run black .When to reach for this:
- Ending formatting debates in code review
- Enforcing consistent style across a team
- Pre-commit hooks that auto-format before commit
Working Example
# Before formatting
def calculate_total(items, tax_rate=0.1, discount=0):
subtotal=sum(i.price*i.qty for i in items)
return subtotal*(1+tax_rate)-discount
# After: uv run ruff format file.py
def calculate_total(items, tax_rate=0.1, discount=0):
subtotal = sum(i.price * i.qty for i in items)
return subtotal * (1 + tax_rate) - discount[tool.ruff.format]
line-length = 88
quote-style = "double"
indent-style = "space"uv run ruff format --check . # CI: fail if unformatted
uv run ruff format . # fix locallyWhat this demonstrates:
- Formatter normalizes spacing, quotes, and line breaks
- Output is deterministic: same input always produces same output
--checkmode for CI without modifying files- Config in
pyproject.tomlapplies project-wide
Deep Dive
How It Works
- The formatter parses Python into an AST, then re-emits with consistent rules
- Line length (default 88) guides wrapping decisions
- String normalization (double quotes by default) is configurable
- Ruff format aims for >99% Black compatibility
Black vs Ruff Format
| Aspect | Black | Ruff Format |
|---|---|---|
| Speed | Moderate | Very fast |
| Config | [tool.black] | [tool.ruff.format] |
| Compatibility | Reference impl | Black-compatible |
| Integration | Separate tool | Same binary as linter |
Gotchas
- Formatting generated code - noisy diffs. Fix: exclude
migrations/,generated/in config. - Running formatter without CI check - drift accumulates. Fix:
ruff format --checkin CI. - Manual style overrides -
# fmt: off/onblocks should be rare. Fix: use sparingly for legitimate cases (long SQL strings). - Different line-length in linter and formatter - E501 fires on formatted code. Fix: ignore E501 in ruff lint; set one
line-length. - Mixing Black and ruff format - subtle output differences cause churn. Fix: pick one; remove the other.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| autopep8 | Minimal changes to existing style | You want full determinism |
| yapf | Google style preference | Team wants Black-compatible |
| Manual formatting | Solo scripts | Any team project |
FAQs
Should I use Black or ruff format?
New projects: ruff format (faster, integrated). Existing Black projects: migrate when convenient.
What line length should I use?
88 (Black default) is the Python community standard. Match your linter config.
How do I skip formatting a block?
# fmt: off before and # fmt: on after the block.
Does the formatter change string quotes?
Yes, by default to double quotes. Configure with quote-style = "single".
Can I format Jupyter notebooks?
ruff format notebook.ipynb (ruff 0.6+). Black also supports notebooks.
Will formatting break my code?
No. Formatters only change whitespace and quote style, never semantics.
How do I format on save in VS Code?
Set "editor.defaultFormatter": "charliermarsh.ruff" and "editor.formatOnSave": true.
Should generated migrations be formatted?
Exclude them. Add exclude = ["migrations"] to avoid noisy diffs.
How do I check one file?
ruff format path/to/file.py or black path/to/file.py.
Does formatting affect import order?
No. Import sorting is ruff check --fix (isort rules), not the formatter.
Related
- Ruff Setup - full ruff configuration
- isort & Import Organization - import sorting
- pre-commit Hooks - format on commit
- Linting & Formatting Best Practices - team policy
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.