Python Ruff and Black: Linting and Formatting Setup
Ruff and Black have become the dominant combination for Python code quality tooling. Ruff is a blazing-fast linter written in Rust that replaces flake8, isort, pyupgrade, pep8-naming, and dozens of other plugins — running 10–100x faster than the tools it replaces. Black is the opinionated auto-formatter that eliminates all formatting debates by enforcing one consistent style across the entire codebase. Together they form a zero-configuration quality baseline that any Python project can adopt in minutes.
Table of Contents
Installation
Both tools can be installed as development dependencies via pip or Poetry. They produce no runtime overhead and should never appear in your production dependencies. Install them once per developer machine or per CI environment and let the pre-commit hook handle enforcement automatically.
# pip
pip install ruff black
# Poetry (as dev dependencies)
poetry add --group dev ruff black
# Check versions
ruff --version # ruff 0.4.x
black --version # black, 24.x.x
# Quick usage without configuration
ruff check . # lint all Python files
ruff check . --fix # auto-fix safe issues
black . # format all Python files
black --check . # check formatting without modifying
Ruff Configuration
Ruff reads configuration from pyproject.toml, ruff.toml or .ruff.toml. The pyproject.toml approach is preferred as it keeps all tool configuration in one file. The most important settings are line-length (match your Black setting), select (which rule sets to enable), and target-version (your minimum Python version, which affects which syntax upgrades are suggested).
# pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py311"
src = ["src", "tests"]
exclude = [
".git", ".venv", "__pycache__",
"migrations", "*.pyi",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes (unused imports, undefined names)
"I", # isort (import ordering)
"N", # pep8-naming
"UP", # pyupgrade (modernize syntax)
"B", # flake8-bugbear (common bug patterns)
"C4", # flake8-comprehensions (simplify comprehensions)
"SIM", # flake8-simplify
"ANN", # flake8-annotations (type hints)
"TCH", # flake8-type-checking (TYPE_CHECKING blocks)
"RUF", # ruff-specific rules
]
ignore = [
"E501", # line-length handled by Black
"ANN101", # missing self type annotation
"ANN102", # missing cls type annotation
]
fixable = ["ALL"] # allow --fix on all fixable rules
unfixable = ["F841"] # don't auto-remove unused variables
[tool.ruff.lint.isort]
known-first-party = ["my_package"]
combine-as-imports = true
force-sort-within-sections = true
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["ANN", "S101"] # no type hints or assert ban in tests
"migrations/*.py" = ["ALL"]
Black Configuration
Black is intentionally opinionated with minimal configuration options. The only settings most projects need are line-length and target-version. Black's lack of configuration is a feature — it removes all formatting debates from code review and enforces a single style that every developer, editor, and CI pipeline agrees on.
# pyproject.toml
[tool.black]
line-length = 88
target-version = ["py311", "py312"]
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.venv
| build
| dist
| migrations
)/
'''
# Format all files
black .
# Format a specific file
black src/myapp/api.py
# Check without modifying (useful in CI)
black --check .
# Show diff of what would change
black --diff .
# Format only changed files (with git)
git diff --name-only --diff-filter=ACMR | grep '\.py$' | xargs black
Ruff Rules and Selectors
Ruff implements over 800 lint rules across 50+ rule sets, all enabled via two-letter prefixes. Understanding the most important rule sets lets you tune Ruff's feedback to your team's preferences. Use ruff rule <CODE> to see an explanation of any specific rule, and ruff linter to list all available linters.
# Explain a specific rule
ruff rule B006 # Do not use mutable data structures for argument defaults
# List all available linters
ruff linter
# Check with statistics (see which rules trigger most)
ruff check . --statistics
# Inline suppression (use sparingly)
x = [] # noqa: B006
import os # noqa: F401 — imported but unused
# Suppress an entire file
# ruff: noqa
# Auto-fix and show what was changed
ruff check . --fix --diff
# Examples of issues Ruff catches automatically
# F-string in exception (B028 → use explicit stacklevel)
import warnings
warnings.warn(f"deprecated: {name}") # B028: no stacklevel
# Mutable default argument (B006)
def append_to(element, to=[]): # B006: mutable default
to.append(element)
return to
# Unnecessary list comprehension (C416)
result = list(x for x in range(10)) # → list(range(10))
# Use f-string instead of format (UP032)
msg = "Hello {}".format(name) # → f"Hello {name}"
# Comparison to None (E711)
if x == None: # → if x is None:
pass
Pre-commit Hooks
Pre-commit hooks enforce linting and formatting on every commit, preventing bad code from ever entering the repository. The pre-commit framework runs configured hooks automatically before git commit completes. Ruff and Black both provide official pre-commit hooks that run in isolated environments — they don't use the project's virtualenv, ensuring consistent versions across all developers.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.9
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format # use ruff's built-in formatter (Black-compatible)
# OR use Black separately (if you need Black specifically):
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- id: debug-statements
# Install pre-commit
pip install pre-commit
# Install hooks into .git/hooks/
pre-commit install
# Run on all files (initial setup)
pre-commit run --all-files
# Update all hooks to latest versions
pre-commit autoupdate
# Skip hooks for an emergency commit
git commit --no-verify -m "hotfix: bypass hooks"
VS Code Integration
Install the Ruff VS Code extension (astral-sh.ruff-vscode) for real-time linting and format-on-save. Configure VS Code to use Ruff as the default formatter and linter so that the editor experience matches CI exactly — no more "it passed locally" surprises.
// .vscode/settings.json
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
"ruff.enable": true,
"ruff.lint.enable": true,
"ruff.format.enable": true,
"python.linting.enabled": false, // disable old pylint/flake8
"python.formatting.provider": "none" // let Ruff handle formatting
}
Migrating from flake8/isort
Ruff is a drop-in replacement for flake8, isort, and most of their popular plugins. The migration is usually a matter of uninstalling the old tools, installing Ruff, and translating your .flake8 / setup.cfg configuration to pyproject.toml. Ruff even provides a migration guide that maps flake8 plugin rules to Ruff selectors.
# Remove old tools
pip uninstall flake8 flake8-bugbear flake8-isort isort pyupgrade
# Or with Poetry
poetry remove --group dev flake8 isort
# Install Ruff
pip install ruff
# Ruff replaces these common tools:
# flake8 → ruff (E, W, F rules)
# flake8-bugbear → ruff (B rules)
# isort → ruff (I rules) — add "I" to select
# pyupgrade → ruff (UP rules)
# pep8-naming → ruff (N rules)
# flake8-comprehensions → ruff (C4 rules)
ruff format) that is Black-compatible with near-identical output. Many teams are replacing Black with ruff format to have a single tool for both linting and formatting, simplifying pre-commit and CI configuration.
CI Enforcement
Add linting and formatting checks as early stages in your CI pipeline. They run in seconds and catch issues before tests run, giving developers fast feedback. Use --check mode for Black and exit-code checking for Ruff to fail the CI build on any violation.
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install tools
run: pip install ruff black
- name: Ruff lint
run: ruff check . --output-format=github # annotates PR with inline comments
- name: Black format check
run: black --check .
# Alternative: use Ruff for everything
- name: Ruff lint + format
run: |
ruff check . --output-format=github
ruff format --check .