893 lines
15 KiB
Markdown
893 lines
15 KiB
Markdown
# Ruff: Complete Guide
|
|
|
|
Ruff is an extremely fast Python linter and code formatter written in Rust. It's 10-100x faster than existing linters and formatters, combining functionality from Flake8, Black, isort, and more into a single tool.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Installation](#installation)
|
|
2. [Linting](#linting)
|
|
3. [Formatting](#formatting)
|
|
4. [Configuration](#configuration)
|
|
5. [Rule Selection](#rule-selection)
|
|
6. [Error Suppression](#error-suppression)
|
|
7. [Editor Integration](#editor-integration)
|
|
8. [CI/CD Integration](#cicd-integration)
|
|
9. [Migration Guide](#migration-guide)
|
|
|
|
## Installation
|
|
|
|
### With UV (Recommended)
|
|
|
|
```bash
|
|
# Install as a tool
|
|
uv tool install ruff
|
|
|
|
# Or add to project
|
|
uv add --dev ruff
|
|
```
|
|
|
|
### With pip
|
|
|
|
```bash
|
|
pip install ruff
|
|
```
|
|
|
|
### With Homebrew
|
|
|
|
```bash
|
|
brew install ruff
|
|
```
|
|
|
|
### With conda
|
|
|
|
```bash
|
|
conda install -c conda-forge ruff
|
|
```
|
|
|
|
### Verify Installation
|
|
|
|
```bash
|
|
ruff version
|
|
# Output: ruff 0.12.8
|
|
```
|
|
|
|
## Linting
|
|
|
|
### Basic Linting
|
|
|
|
```bash
|
|
# Check current directory
|
|
ruff check .
|
|
|
|
# Check specific files
|
|
ruff check src/main.py tests/
|
|
|
|
# Check and auto-fix
|
|
ruff check --fix .
|
|
|
|
# Preview changes without applying
|
|
ruff check --diff .
|
|
|
|
# Show fixes that would be applied
|
|
ruff check --show-fixes .
|
|
```
|
|
|
|
### Watch Mode
|
|
|
|
```bash
|
|
# Continuously check for errors
|
|
ruff check --watch .
|
|
```
|
|
|
|
### Output Formats
|
|
|
|
```bash
|
|
# Default (human-readable)
|
|
ruff check .
|
|
|
|
# JSON format
|
|
ruff check --output-format json .
|
|
|
|
# GitHub Actions format
|
|
ruff check --output-format github .
|
|
|
|
# GitLab format
|
|
ruff check --output-format gitlab .
|
|
|
|
# JUnit XML format
|
|
ruff check --output-format junit .
|
|
```
|
|
|
|
### Unsafe Fixes
|
|
|
|
```bash
|
|
# Include unsafe fixes
|
|
ruff check --fix --unsafe-fixes .
|
|
|
|
# Fix only (don't report remaining violations)
|
|
ruff check --fix-only .
|
|
```
|
|
|
|
### Statistics and Debugging
|
|
|
|
```bash
|
|
# Show statistics for each rule
|
|
ruff check --statistics .
|
|
|
|
# Show files that will be checked
|
|
ruff check --show-files .
|
|
|
|
# Show configuration being used
|
|
ruff check --show-settings .
|
|
```
|
|
|
|
## Formatting
|
|
|
|
### Basic Formatting
|
|
|
|
```bash
|
|
# Format current directory
|
|
ruff format .
|
|
|
|
# Format specific files
|
|
ruff format src/main.py
|
|
|
|
# Check formatting without changes
|
|
ruff format --check .
|
|
|
|
# Show diff of changes
|
|
ruff format --diff .
|
|
```
|
|
|
|
### Format and Lint Together
|
|
|
|
```bash
|
|
# Recommended workflow
|
|
ruff check --fix . && ruff format .
|
|
```
|
|
|
|
### Format Options
|
|
|
|
```bash
|
|
# Use preview features
|
|
ruff format --preview .
|
|
|
|
# Specific target Python version
|
|
ruff format --target-version py312 .
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Configuration Files
|
|
|
|
Ruff looks for configuration in (in order of precedence):
|
|
1. `.ruff.toml`
|
|
2. `ruff.toml`
|
|
3. `pyproject.toml`
|
|
|
|
### Basic Configuration
|
|
|
|
**pyproject.toml:**
|
|
```toml
|
|
[tool.ruff]
|
|
# Set line length
|
|
line-length = 88
|
|
indent-width = 4
|
|
|
|
# Set Python version
|
|
target-version = "py311"
|
|
|
|
# Exclude directories
|
|
exclude = [
|
|
".git",
|
|
".venv",
|
|
"__pycache__",
|
|
"build",
|
|
"dist",
|
|
"*.egg-info",
|
|
]
|
|
|
|
[tool.ruff.lint]
|
|
# Enable specific rules
|
|
select = [
|
|
"E", # pycodestyle errors
|
|
"W", # pycodestyle warnings
|
|
"F", # Pyflakes
|
|
"I", # isort
|
|
"B", # flake8-bugbear
|
|
"UP", # pyupgrade
|
|
"C4", # flake8-comprehensions
|
|
]
|
|
|
|
# Ignore specific rules
|
|
ignore = [
|
|
"E501", # line too long (handled by formatter)
|
|
]
|
|
|
|
# Allow auto-fixing
|
|
fixable = ["ALL"]
|
|
unfixable = []
|
|
|
|
# Allow unused variables prefixed with underscore
|
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
|
|
[tool.ruff.format]
|
|
# Use double quotes
|
|
quote-style = "double"
|
|
|
|
# Use spaces for indentation
|
|
indent-style = "space"
|
|
|
|
# Respect magic trailing commas
|
|
skip-magic-trailing-comma = false
|
|
|
|
# Auto-detect line endings
|
|
line-ending = "auto"
|
|
|
|
# Format code in docstrings
|
|
docstring-code-format = true
|
|
docstring-code-line-length = 72
|
|
```
|
|
|
|
### Per-File Configuration
|
|
|
|
```toml
|
|
[tool.ruff.lint.per-file-ignores]
|
|
# Ignore unused imports in __init__.py
|
|
"__init__.py" = ["F401"]
|
|
|
|
# Ignore assert statements in tests
|
|
"tests/*" = ["S101"]
|
|
|
|
# Ignore print statements in scripts
|
|
"scripts/*" = ["T201"]
|
|
|
|
# Ignore import order in migrations
|
|
"migrations/*" = ["I"]
|
|
```
|
|
|
|
### Import Sorting (isort)
|
|
|
|
```toml
|
|
[tool.ruff.lint.isort]
|
|
# Known first-party packages
|
|
known-first-party = ["myproject"]
|
|
|
|
# Known third-party packages
|
|
known-third-party = ["django", "requests"]
|
|
|
|
# Section order
|
|
section-order = [
|
|
"future",
|
|
"standard-library",
|
|
"third-party",
|
|
"first-party",
|
|
"local-folder"
|
|
]
|
|
|
|
# Combine as imports
|
|
combine-as-imports = true
|
|
|
|
# Split on trailing comma
|
|
split-on-trailing-comma = true
|
|
```
|
|
|
|
### Docstring Configuration
|
|
|
|
```toml
|
|
[tool.ruff.lint.pydocstyle]
|
|
# Use Google-style docstrings
|
|
convention = "google" # or "numpy", "pep257"
|
|
```
|
|
|
|
## Rule Selection
|
|
|
|
### Available Rule Sets
|
|
|
|
| Code | Name | Description |
|
|
|------|------|-------------|
|
|
| E | pycodestyle errors | PEP 8 error codes |
|
|
| W | pycodestyle warnings | PEP 8 warning codes |
|
|
| F | Pyflakes | Logical errors |
|
|
| I | isort | Import sorting |
|
|
| N | pep8-naming | Naming conventions |
|
|
| D | pydocstyle | Docstring style |
|
|
| UP | pyupgrade | Modern Python syntax |
|
|
| B | flake8-bugbear | Common bugs |
|
|
| A | flake8-builtins | Builtin shadowing |
|
|
| C4 | flake8-comprehensions | List/dict/set comprehensions |
|
|
| T20 | flake8-print | Print statements |
|
|
| PT | flake8-pytest-style | Pytest style |
|
|
| S | flake8-bandit | Security issues |
|
|
| Q | flake8-quotes | Quote style |
|
|
| RUF | Ruff-specific | Ruff custom rules |
|
|
|
|
### Selecting Rules
|
|
|
|
```bash
|
|
# Enable specific rule set
|
|
ruff check --select E,W,F .
|
|
|
|
# Enable all rules
|
|
ruff check --select ALL .
|
|
|
|
# Extend default rules
|
|
ruff check --extend-select B,I .
|
|
|
|
# Ignore specific rules
|
|
ruff check --ignore E501,W503 .
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```toml
|
|
[tool.ruff.lint]
|
|
# Start with Flake8 defaults and add more
|
|
select = ["E", "F"]
|
|
extend-select = ["B", "I", "N"]
|
|
|
|
# Ignore specific rules
|
|
ignore = ["E501"]
|
|
extend-ignore = ["W503"]
|
|
|
|
# Make specific rules fixable
|
|
fixable = ["I", "F401"]
|
|
|
|
# Make specific rules non-fixable
|
|
unfixable = ["B"]
|
|
```
|
|
|
|
### Popular Rule Combinations
|
|
|
|
**Minimal (Default):**
|
|
```toml
|
|
select = ["E4", "E7", "E9", "F"]
|
|
```
|
|
|
|
**Standard:**
|
|
```toml
|
|
select = ["E", "W", "F", "I"]
|
|
```
|
|
|
|
**Strict:**
|
|
```toml
|
|
select = [
|
|
"E", # pycodestyle errors
|
|
"W", # pycodestyle warnings
|
|
"F", # Pyflakes
|
|
"I", # isort
|
|
"N", # pep8-naming
|
|
"D", # pydocstyle
|
|
"UP", # pyupgrade
|
|
"B", # flake8-bugbear
|
|
"C4", # flake8-comprehensions
|
|
"S", # flake8-bandit
|
|
"T20", # flake8-print
|
|
"PT", # flake8-pytest-style
|
|
]
|
|
```
|
|
|
|
## Error Suppression
|
|
|
|
### Inline Comments
|
|
|
|
```python
|
|
# Ignore specific rule on a line
|
|
import os # noqa: F401
|
|
|
|
# Ignore multiple rules
|
|
import sys, os # noqa: F401, E401
|
|
|
|
# Ignore all rules on a line
|
|
very_long_variable_name = "value" # noqa
|
|
|
|
# Ignore for next line
|
|
# noqa: E501
|
|
very_long_line = "This is a very long line that exceeds the limit"
|
|
```
|
|
|
|
### File-Level Suppression
|
|
|
|
```python
|
|
# At top of file
|
|
# ruff: noqa: F401, E402
|
|
|
|
import os
|
|
import sys
|
|
```
|
|
|
|
### Auto-Add noqa Comments
|
|
|
|
```bash
|
|
# Automatically add noqa comments
|
|
ruff check --add-noqa .
|
|
```
|
|
|
|
### Type-Aware Suppression
|
|
|
|
```python
|
|
# Type checkers only
|
|
import TYPE_CHECKING # type: ignore
|
|
|
|
# Ruff specific
|
|
from typing import Optional # ruff: noqa: UP007
|
|
```
|
|
|
|
## Editor Integration
|
|
|
|
### VS Code
|
|
|
|
Install the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff):
|
|
|
|
**.vscode/settings.json:**
|
|
```json
|
|
{
|
|
"[python]": {
|
|
"editor.formatOnSave": true,
|
|
"editor.codeActionsOnSave": {
|
|
"source.fixAll.ruff": "explicit",
|
|
"source.organizeImports.ruff": "explicit"
|
|
},
|
|
"editor.defaultFormatter": "charliermarsh.ruff"
|
|
},
|
|
"ruff.lint.args": ["--config=pyproject.toml"],
|
|
"ruff.format.args": ["--config=pyproject.toml"]
|
|
}
|
|
```
|
|
|
|
### PyCharm/IntelliJ
|
|
|
|
1. Install Ruff via system package manager
|
|
2. Configure as external tool:
|
|
- File → Settings → Tools → External Tools
|
|
- Add Ruff with appropriate arguments
|
|
|
|
### Vim/Neovim
|
|
|
|
**With ALE:**
|
|
```vim
|
|
let g:ale_linters = {'python': ['ruff']}
|
|
let g:ale_fixers = {'python': ['ruff']}
|
|
```
|
|
|
|
**With nvim-lspconfig:**
|
|
```lua
|
|
require('lspconfig').ruff_lsp.setup{}
|
|
```
|
|
|
|
### Emacs
|
|
|
|
```elisp
|
|
;; With flycheck
|
|
(require 'flycheck)
|
|
(flycheck-define-checker python-ruff
|
|
"A Python checker using ruff."
|
|
:command ("ruff" "check" "--output-format" "text" source)
|
|
:error-patterns
|
|
((error line-start (file-name) ":" line ":" column ": " (message) line-end))
|
|
:modes python-mode)
|
|
(add-to-list 'flycheck-checkers 'python-ruff)
|
|
```
|
|
|
|
## CI/CD Integration
|
|
|
|
### GitHub Actions
|
|
|
|
```yaml
|
|
name: Lint with Ruff
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install uv
|
|
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
|
|
- name: Install dependencies
|
|
run: uv sync
|
|
|
|
- name: Lint with Ruff
|
|
run: uv run ruff check .
|
|
|
|
- name: Format check with Ruff
|
|
run: uv run ruff format --check .
|
|
```
|
|
|
|
**Using Ruff Action:**
|
|
```yaml
|
|
- uses: chartboost/ruff-action@v1
|
|
with:
|
|
args: check --output-format github
|
|
```
|
|
|
|
### GitLab CI
|
|
|
|
```yaml
|
|
ruff:
|
|
image: python:3.12
|
|
before_script:
|
|
- pip install ruff
|
|
script:
|
|
- ruff check .
|
|
- ruff format --check .
|
|
```
|
|
|
|
### Pre-commit Hooks
|
|
|
|
**.pre-commit-config.yaml:**
|
|
```yaml
|
|
repos:
|
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
rev: v0.12.8
|
|
hooks:
|
|
# Run the linter
|
|
- id: ruff
|
|
args: [--fix]
|
|
# Run the formatter
|
|
- id: ruff-format
|
|
```
|
|
|
|
Install:
|
|
```bash
|
|
pip install pre-commit
|
|
pre-commit install
|
|
```
|
|
|
|
### Docker
|
|
|
|
```dockerfile
|
|
FROM python:3.12-slim
|
|
|
|
# Install Ruff
|
|
RUN pip install ruff
|
|
|
|
# Copy code
|
|
COPY . /app
|
|
WORKDIR /app
|
|
|
|
# Run Ruff
|
|
RUN ruff check .
|
|
RUN ruff format --check .
|
|
```
|
|
|
|
## Migration Guide
|
|
|
|
### From Flake8
|
|
|
|
```bash
|
|
# Flake8 configuration
|
|
[flake8]
|
|
max-line-length = 88
|
|
extend-ignore = E203, W503
|
|
exclude = .git,__pycache__,build,dist
|
|
```
|
|
|
|
```toml
|
|
# Equivalent Ruff configuration
|
|
[tool.ruff]
|
|
line-length = 88
|
|
|
|
[tool.ruff.lint]
|
|
ignore = ["E203", "W503"]
|
|
exclude = [".git", "__pycache__", "build", "dist"]
|
|
```
|
|
|
|
### From Black
|
|
|
|
Ruff format is designed to be a drop-in replacement for Black:
|
|
|
|
```bash
|
|
# Replace
|
|
black .
|
|
|
|
# With
|
|
ruff format .
|
|
```
|
|
|
|
Configuration is compatible:
|
|
```toml
|
|
[tool.black]
|
|
line-length = 88
|
|
target-version = ["py311"]
|
|
|
|
# Becomes
|
|
[tool.ruff]
|
|
line-length = 88
|
|
target-version = "py311"
|
|
```
|
|
|
|
### From isort
|
|
|
|
```bash
|
|
# Replace
|
|
isort .
|
|
|
|
# With
|
|
ruff check --select I --fix .
|
|
# Or
|
|
ruff format . # Includes import sorting
|
|
```
|
|
|
|
Configuration:
|
|
```toml
|
|
[tool.isort]
|
|
profile = "black"
|
|
known_first_party = ["myproject"]
|
|
|
|
# Becomes
|
|
[tool.ruff.lint.isort]
|
|
known-first-party = ["myproject"]
|
|
```
|
|
|
|
### From pyupgrade
|
|
|
|
```bash
|
|
# Replace
|
|
pyupgrade --py311-plus **/*.py
|
|
|
|
# With
|
|
ruff check --select UP --fix .
|
|
```
|
|
|
|
### Complete Migration
|
|
|
|
**Before:**
|
|
```bash
|
|
# Multiple tools
|
|
isort .
|
|
black .
|
|
flake8 .
|
|
pyupgrade --py311-plus **/*.py
|
|
```
|
|
|
|
**After:**
|
|
```bash
|
|
# Single command
|
|
ruff check --fix . && ruff format .
|
|
```
|
|
|
|
**Configuration consolidation:**
|
|
```toml
|
|
# pyproject.toml - All in one place
|
|
[tool.ruff]
|
|
line-length = 88
|
|
target-version = "py311"
|
|
|
|
[tool.ruff.lint]
|
|
select = ["E", "W", "F", "I", "UP", "B"]
|
|
ignore = ["E501"]
|
|
|
|
[tool.ruff.format]
|
|
quote-style = "double"
|
|
```
|
|
|
|
## Common Rules Explained
|
|
|
|
### E/W (pycodestyle)
|
|
|
|
```python
|
|
# E501: Line too long
|
|
very_long_line = "This line exceeds 88 characters and will be flagged by E501"
|
|
|
|
# E401: Multiple imports on one line
|
|
import os, sys # Bad
|
|
|
|
# E402: Module level import not at top
|
|
def foo():
|
|
pass
|
|
import os # Bad
|
|
|
|
# W503: Line break before binary operator
|
|
result = (value
|
|
+ other_value) # Warning in older style guides
|
|
```
|
|
|
|
### F (Pyflakes)
|
|
|
|
```python
|
|
# F401: Imported but unused
|
|
import os # Not used anywhere
|
|
|
|
# F841: Local variable assigned but never used
|
|
def foo():
|
|
x = 10 # Never used
|
|
|
|
# F821: Undefined name
|
|
print(undefined_var) # NameError at runtime
|
|
```
|
|
|
|
### I (isort)
|
|
|
|
```python
|
|
# Incorrect import order
|
|
from myproject import foo
|
|
import os
|
|
import sys
|
|
from third_party import bar
|
|
|
|
# Correct
|
|
import os
|
|
import sys
|
|
|
|
from third_party import bar
|
|
|
|
from myproject import foo
|
|
```
|
|
|
|
### UP (pyupgrade)
|
|
|
|
```python
|
|
# UP006: Use list instead of List from typing
|
|
from typing import List # Old
|
|
def foo() -> List[int]: # Old
|
|
pass
|
|
|
|
# New
|
|
def foo() -> list[int]: # New (Python 3.9+)
|
|
pass
|
|
|
|
# UP032: Use f-string instead of format
|
|
"{} {}".format(a, b) # Old
|
|
f"{a} {b}" # New
|
|
```
|
|
|
|
### B (flake8-bugbear)
|
|
|
|
```python
|
|
# B006: Mutable default argument
|
|
def foo(items=[]): # Bad - mutable default
|
|
items.append(1)
|
|
return items
|
|
|
|
# B008: Do not perform function call in default argument
|
|
def foo(timestamp=datetime.now()): # Bad
|
|
pass
|
|
|
|
# B011: Do not use assert False
|
|
assert False, "This should not happen" # Use raise instead
|
|
```
|
|
|
|
## Advanced Features
|
|
|
|
### Rule Explainer
|
|
|
|
```bash
|
|
# Get detailed explanation of a rule
|
|
ruff rule E501
|
|
|
|
# List all rules
|
|
ruff rule --all
|
|
|
|
# Search for rules
|
|
ruff rule --filter "import"
|
|
```
|
|
|
|
### Custom Configuration per Directory
|
|
|
|
```toml
|
|
# Root pyproject.toml
|
|
[tool.ruff]
|
|
line-length = 88
|
|
|
|
# tests/ can have different config
|
|
```
|
|
|
|
Create `tests/pyproject.toml`:
|
|
```toml
|
|
[tool.ruff]
|
|
extend = "../pyproject.toml" # Inherit root config
|
|
line-length = 120 # Override for tests
|
|
```
|
|
|
|
### Preview Mode
|
|
|
|
```bash
|
|
# Enable preview features
|
|
ruff check --preview .
|
|
ruff format --preview .
|
|
```
|
|
|
|
```toml
|
|
[tool.ruff]
|
|
preview = true
|
|
```
|
|
|
|
### Unsafe Fixes
|
|
|
|
Some fixes are marked as "unsafe" because they might change semantics:
|
|
|
|
```bash
|
|
# Include unsafe fixes
|
|
ruff check --fix --unsafe-fixes .
|
|
```
|
|
|
|
### Cache Management
|
|
|
|
```bash
|
|
# Clear Ruff cache
|
|
ruff clean
|
|
|
|
# Disable cache
|
|
ruff check --no-cache .
|
|
```
|
|
|
|
## Performance Tips
|
|
|
|
1. **Use --no-cache for CI**: Ensures fresh analysis
|
|
2. **Run in parallel**: Ruff already parallelizes internally
|
|
3. **Use specific selects**: Only enable rules you need
|
|
4. **Exclude large directories**: Skip `node_modules`, `venv`, etc.
|
|
5. **Use fix in CI**: Auto-fix what you can, then check remaining
|
|
|
|
## Troubleshooting
|
|
|
|
### Ruff Not Found
|
|
|
|
```bash
|
|
# Check installation
|
|
which ruff
|
|
ruff version
|
|
|
|
# Reinstall
|
|
uv tool uninstall ruff
|
|
uv tool install ruff
|
|
```
|
|
|
|
### Configuration Not Loaded
|
|
|
|
```bash
|
|
# Show active configuration
|
|
ruff check --show-settings .
|
|
|
|
# Use specific config file
|
|
ruff check --config path/to/pyproject.toml .
|
|
```
|
|
|
|
### Rules Not Working
|
|
|
|
```bash
|
|
# Check which rules are enabled
|
|
ruff rule --all | grep E501
|
|
|
|
# Explain specific rule
|
|
ruff rule E501
|
|
```
|
|
|
|
### Performance Issues
|
|
|
|
```bash
|
|
# Profile ruff execution
|
|
time ruff check .
|
|
|
|
# Check file discovery
|
|
ruff check --show-files .
|
|
|
|
# Clear cache
|
|
ruff clean
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Start with defaults**, add rules gradually
|
|
2. **Use format + lint together** in your workflow
|
|
3. **Configure in pyproject.toml** for centralized settings
|
|
4. **Use per-file-ignores** sparingly
|
|
5. **Enable auto-fix in pre-commit** hooks
|
|
6. **Run in CI/CD** to enforce standards
|
|
7. **Use --diff in CI** to show what would change
|
|
8. **Document rule exceptions** with inline comments
|
|
9. **Keep Ruff updated** for new features and fixes
|
|
10. **Use editor integration** for real-time feedback
|
|
|
|
## Resources
|
|
|
|
- [Ruff Documentation](https://docs.astral.sh/ruff/)
|
|
- [Ruff GitHub](https://github.com/astral-sh/ruff)
|
|
- [Ruff Rules](https://docs.astral.sh/ruff/rules/)
|
|
- [Ruff Settings](https://docs.astral.sh/ruff/settings/)
|
|
- [Ruff VS Code Extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff)
|