black Best Practices
black is the uncompromising Python code formatter. Our team adopts black as the definitive style guide for all Python projects. By using black, you cede control over formatting minutiae, gaining speed, determinism, and freedom from style debates.
🚨 IMPORTANT: For new projects, or if migrating, strongly consider Ruff instead of black. Ruff offers superior performance and combines formatting, linting, and import sorting into a single tool. If your project is already committed to black, these guidelines apply.
1. Core Principle: Cede Control
black is opinionated by design. Do not fight its formatting. Embrace its defaults to achieve ultimate consistency across the codebase.
2. Installation and Configuration
Always install the latest stable version of black (v25.12.0 as of Dec 2025) and configure it via pyproject.toml.
2.1. Project Dependency
Declare black as a development dependency.
❌ BAD: Global installation or missing dependency
# pyproject.toml
# black not listed, relies on global install or manual management
✅ GOOD: black as a development dependency
# pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
[tool.poetry.group.dev.dependencies]
black = "^25.12.0" # Always pin to latest major/minor
2.2. pyproject.toml Configuration
Stick to black's defaults. Only configure line length if absolutely necessary for legacy reasons (e.g., integrating with an older codebase that has a different standard).
❌ BAD: Over-configuring black
# pyproject.toml
[tool.black]
line-length = 100 # Deviating from 88 without strong reason
skip-string-normalization = true # Disabling core black features
✅ GOOD: Minimal black configuration (default 88 chars)
# pyproject.toml
[tool.black]
# No configuration needed; defaults are best.
# If you must change line-length, do so sparingly:
# line-length = 88
3. Automated Formatting Workflow
Integrate black into your development pipeline to ensure continuous consistency.
3.1. Pre-commit Hooks
Use pre-commit to automatically format files before every commit. This catches formatting issues early.
❌ BAD: Manual formatting or relying on CI only
# Developer forgets to run black before committing
git commit -m "feat: new feature" # Unformatted code committed
✅ GOOD: black via pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 25.12.0 # Use the latest stable version
hooks:
- id: black
language_version: python3.11 # Match your project's Python version
Action: Install pre-commit (pip install pre-commit) and set up hooks (pre-commit install).
3.2. CI/CD Integration
Ensure your CI pipeline checks for black compliance and fails the build on any formatting mismatch.
❌ BAD: CI passes even with unformatted code
# In CI script
# No black check, or black --diff only
✅ GOOD: CI enforces black formatting
# In CI script (e.g., .github/workflows/main.yml)
- name: Check code style with Black
run: pip install black==25.12.0 && black --check .
4. Code Organization and Structure
black ensures consistent formatting, which indirectly improves code organization and readability.
4.1. Import Sorting
black does not sort imports. Pair it with isort (or Ruff's built-in import sorting) for a complete solution.
❌ BAD: Unsorted imports
import os
from my_module import ClassA
import sys
from third_party import lib
✅ GOOD: Sorted imports (with isort or Ruff)
import os
import sys
from third_party import lib
from my_module import ClassA
Action: Add isort to your pre-commit-config.yaml or use Ruff.
4.2. Consistent String Quotes and Trailing Commas
black enforces double quotes for strings and trailing commas in multi-line constructs.
❌ BAD: Inconsistent quotes, missing trailing commas
my_string = 'hello world'
my_list = [
1,
2
]
✅ GOOD: black-formatted strings and lists
my_string = "hello world"
my_list = [
1,
2, # Trailing comma for multi-line
]
5. Type Hints
black consistently formats type hints, making them more readable. Always use explicit type hints.
❌ BAD: Inconsistent spacing or missing type hints
def process_data(data : list):
return len(data)
def get_name(user):
return user.name
✅ GOOD: black-formatted and explicit type hints
from typing import Any
def process_data(data: list[Any]) -> int:
return len(data)
def get_name(user: Any) -> str:
return user.name
6. Testing Approaches
black ensures your test files are as consistently formatted and readable as your application code. This aids in test maintenance and debugging.
❌ BAD: Unformatted or inconsistently formatted test files
def test_addition():
assert 1+1 == 2
✅ GOOD: black-formatted test files
def test_addition():
assert 1 + 1 == 2
Action: Ensure black runs on your tests/ directory.
7. Common Pitfalls and Gotchas
7.1. Version Mismatches
Inconsistent black versions across the team or CI can lead to re-formatting churn.
❌ BAD: Different black versions in use
# Dev A uses black 24.1.0, Dev B uses black 25.12.0
# Code formatted by A gets re-formatted by B, leading to noisy diffs.
✅ GOOD: Standardized black version
# pyproject.toml
[tool.poetry.group.dev.dependencies]
black = "==25.12.0" # Pin to exact version for consistency
Action: Pin black to an exact version in pyproject.toml and pre-commit-config.yaml.
7.2. Ignoring black
Disabling black for specific files or sections without a strong, documented reason undermines consistency.
❌ BAD: Disabling black for convenience
# my_module.py
# fmt: off
def ugly_function():
# ... unformatted code ...
# fmt: on
✅ GOOD: Trust black's formatting
# my_module.py
def ugly_function():
# black will format this consistently
result = (
"a_very_long_string_that_black_will_wrap_nicely"
+ "another_part_of_the_string"
)
return result
Action: Only use fmt: off/fmt: on directives as a last resort for genuinely un-black-able code, and document why.