Linting with Ruff¶
Ruff is the modern, ultrafast Python linter that replaces Flake8, isort, pydocstyle, pyupgrade, and more. It's 100x faster than traditional tools and written in Rust for maximum performance.
Why Ruff for Linting?¶
Key Benefits
- 100x faster than Flake8, pylint, and other linters
- Replaces multiple tools (Flake8, isort, pyupgrade, pydocstyle, etc.)
- 800+ built-in rules covering style, bugs, security, and complexity
- Auto-fix for most issues
- Zero configuration to get started
- Active development by Astral (creators of uv)
- Single tool for both linting and formatting
Traditional Python linting requires a complex toolchain:
- Flake8 for style
- isort for import sorting
- pyupgrade for syntax modernization
- pydocstyle for docstring conventions
- Bandit for security
- McCabe for complexity
Ruff replaces all of these with a single, fast binary.
Installation and Quick Start¶
Installation¶
Quick Start¶
# Lint all Python files
ruff check .
# Lint and auto-fix issues
ruff check --fix .
# Lint with unsafe fixes (more aggressive)
ruff check --fix --unsafe-fixes .
# Show what would be fixed (dry run)
ruff check --fix --diff .
# Lint specific file
ruff check myfile.py
That's it! Ruff works great with zero configuration.
Configuration in pyproject.toml¶
Basic Configuration¶
[tool.ruff]
# Exclude common directories
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
"migrations", # Django migrations
]
# Line length for linting (should match formatter)
line-length = 180
# Target Python version
target-version = "py313"
Rule Selection¶
[tool.ruff.lint]
# Select rule categories to enable
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"DJ", # flake8-django
"PIE", # flake8-pie
"T20", # flake8-print
"Q", # flake8-quotes
"RET", # flake8-return
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"ARG", # flake8-unused-arguments
"PTH", # flake8-use-pathlib
"ERA", # eradicate (commented-out code)
"PL", # Pylint
"RUF", # Ruff-specific rules
]
# Ignore specific rules
ignore = [
"F403", # 'from module import *' used; unable to detect undefined names
]
# Allow auto-fix for all enabled rules
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
Per-File Ignores¶
[tool.ruff.lint.per-file-ignores]
# Ignore import violations in __init__.py
"__init__.py" = ["F401"]
# Ignore print statements in scripts
"scripts/*.py" = ["T201"]
# Ignore all checks in migrations
"*/migrations/*.py" = ["ALL"]
# Allow unused imports in tests (fixtures)
"tests/*.py" = ["F401", "ARG001"]
Rule Selection Strategy¶
Understanding Rule Categories¶
Ruff organizes rules into categories. Here's how to choose:
Start Small, Grow Gradually
Enable rules incrementally. Start with core rules, then add more as your team adjusts.
Essential Rules (Start Here)¶
[tool.ruff.lint]
select = [
"E", # pycodestyle errors (PEP 8 violations)
"F", # Pyflakes (undefined names, unused imports)
"I", # isort (import sorting)
]
These catch critical errors and maintain basic code quality.
Recommended Rules (Add Next)¶
select = [
"E", "F", "I", # Essential (above)
"W", # pycodestyle warnings
"N", # pep8-naming (naming conventions)
"UP", # pyupgrade (modern Python syntax)
"B", # flake8-bugbear (common bugs)
]
These improve code quality without being overly strict.
Advanced Rules (Optional)¶
select = [
# ... previous rules ...
"C4", # flake8-comprehensions (better comprehensions)
"DJ", # flake8-django (Django best practices)
"PIE", # flake8-pie (misc lints)
"T20", # flake8-print (detect print statements)
"Q", # flake8-quotes (quote consistency)
"RET", # flake8-return (return statement consistency)
"SIM", # flake8-simplify (code simplification)
"ARG", # flake8-unused-arguments
"PTH", # flake8-use-pathlib (prefer pathlib)
"ERA", # eradicate (commented-out code)
"PL", # Pylint (comprehensive checks)
"RUF", # Ruff-specific rules
]
Rule Categories Reference¶
| Category | Name | Purpose | Strictness |
|---|---|---|---|
E |
pycodestyle errors | PEP 8 violations | Essential |
F |
Pyflakes | Undefined names, imports | Essential |
I |
isort | Import sorting | Essential |
W |
pycodestyle warnings | Style warnings | Recommended |
N |
pep8-naming | Naming conventions | Recommended |
UP |
pyupgrade | Modern syntax | Recommended |
B |
flake8-bugbear | Common bugs | Recommended |
C4 |
comprehensions | List/dict comprehensions | Optional |
DJ |
flake8-django | Django patterns | Django-specific |
PIE |
flake8-pie | Misc improvements | Optional |
T20 |
flake8-print | Print detection | Optional |
Q |
flake8-quotes | Quote style | Optional |
RET |
flake8-return | Return consistency | Optional |
SIM |
flake8-simplify | Code simplification | Optional |
TCH |
type-checking | Type checking imports | Type-hint projects |
ARG |
unused-arguments | Unused arguments | Strict |
PTH |
use-pathlib | Prefer pathlib | Strict |
ERA |
eradicate | Commented code | Strict |
PL |
Pylint | Comprehensive | Very strict |
RUF |
Ruff-specific | Ruff extras | Recommended |
Common Patterns and When to Ignore Rules¶
Wildcard Imports (F403, F405)¶
# Often needed in __init__.py or settings
from .models import * # noqa: F403
# Better: explicit imports
from .models import User, Product, Order
When to ignore: Django settings files, package __init__.py files
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F403", "F405"]
"settings/*.py" = ["F403", "F405"]
Print Statements (T201)¶
# Catch leftover debug prints
print("Debug info") # T201: `print` found
# OK in scripts
if __name__ == "__main__":
print("Running migration...")
When to ignore: CLI scripts, management commands
Unused Imports (F401)¶
# Often needed for re-exports
from .views import UserView # noqa: F401
# Or unused test fixtures
@pytest.fixture
def user(): # F401 if not directly used in this file
return User.objects.create(name="Test")
When to ignore: Package __init__.py, test fixture files
Django Migrations (ALL)¶
Always ignore: All migration files
Commented Code (ERA001)¶
# Sometimes you need to keep code commented
# for documentation or future reference
# old_function() # noqa: ERA001
When to ignore: Temporarily during development
Use Sparingly
Commented code should usually be deleted. Use version control for history.
Line-Level Ignores¶
# Ignore specific rule on one line
from module import * # noqa: F403
# Ignore multiple rules
x = 1; y = 2 # noqa: E702, E701
# Ignore all rules (avoid!)
bad_code() # noqa
Prefer File-Level Ignores
Use per-file-ignores in config instead of inline # noqa comments when possible.
Integration with Editors¶
VS Code¶
Install the official Ruff extension:
- Open Extensions (Ctrl+Shift+X)
- Search for "Ruff"
- Install "Ruff" by Astral Software
.vscode/settings.json:
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
},
"ruff.lint.enable": true,
"ruff.lint.run": "onSave",
"ruff.format.enable": true
}
This enables: - Automatic linting on save - Auto-fix on save - Import organization - Inline error highlighting
PyCharm / IntelliJ¶
- Install Ruff plugin:
- Settings → Plugins
- Search "Ruff"
-
Install and restart
-
Configure Ruff:
- Settings → Tools → Ruff
- Enable "Run Ruff on save"
-
Enable "Show Ruff warnings"
-
External Tool (Alternative):
- Settings → Tools → External Tools
- Add new tool:
- Name: Ruff Lint
- Program:
ruff - Arguments:
check --fix $FilePath$ - Working directory:
$ProjectFileDir$
Vim/Neovim¶
With ALE:
let g:ale_linters = {'python': ['ruff']}
let g:ale_fixers = {'python': ['ruff']}
let g:ale_fix_on_save = 1
With null-ls (Neovim):
local null_ls = require("null-ls")
null_ls.setup({
sources = {
null_ls.builtins.diagnostics.ruff,
null_ls.builtins.formatting.ruff,
},
})
Emacs¶
With flycheck:
Pre-commit Integration¶
.pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
# Linter with auto-fix
- id: ruff
args:
- --fix
- --unsafe-fixes
- --select=T201,T203,W191,W291,W292,W605,LOG001,LOG002,LOG007,LOG009,PLC0206,PLC0208,Q000,Q001,Q002,Q003
# Formatter
- id: ruff-format
Pre-commit Hook Strategy
The example above shows a selective approach: only auto-fix specific safe rules in pre-commit, while running full checks in CI.
Aggressive Pre-commit (Full Auto-fix)¶
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff
args: [--fix, --unsafe-fixes]
- id: ruff-format
Conservative Pre-commit (Check Only)¶
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff
args: [--no-fix] # Only check, don't fix
- id: ruff-format
args: [--check] # Only check formatting
Recommendation: Use selective auto-fix in pre-commit, enforce full rules in CI.
CI/CD Integration¶
GitHub Actions¶
name: Lint
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v1
with:
version: "latest"
- name: Install Ruff
run: uv pip install ruff
- name: Run Ruff linter
run: ruff check .
- name: Check formatting
run: ruff format --check .
GitHub Actions (Ruff Action)¶
GitLab CI¶
ruff-lint:
image: python:3.13-slim
before_script:
- pip install ruff
script:
- ruff check .
- ruff format --check .
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
CircleCI¶
version: 2.1
jobs:
lint:
docker:
- image: cimg/python:3.13
steps:
- checkout
- run:
name: Install Ruff
command: pip install ruff
- run:
name: Run Ruff
command: ruff check .
Comparison with Alternatives¶
Ruff vs Flake8¶
| Feature | Flake8 | Ruff |
|---|---|---|
| Speed | Baseline (1x) | 100x faster |
| Rules | ~200 (with plugins) | 800+ built-in |
| Auto-fix | Via autopep8 | Native auto-fix |
| Import sorting | Requires isort | Built-in |
| Pyupgrade | Requires pyupgrade | Built-in |
| Configuration | Multiple files | Single pyproject.toml |
| Maintenance | Mature, stable | Active, modern |
Verdict: Ruff is the modern replacement for Flake8.
Ruff vs Pylint¶
| Feature | Pylint | Ruff |
|---|---|---|
| Speed | Slow | 100x faster |
| Rules | ~400 comprehensive | 800+ focused |
| False positives | More common | Fewer |
| Configuration | Complex | Simple |
| Auto-fix | Limited | Extensive |
| Code complexity | ✓ (good) | ✓ (available) |
Verdict: Use Ruff for speed, Pylint for deep analysis (if needed).
Ruff vs PyRight/mypy¶
| Feature | PyRight/mypy | Ruff |
|---|---|---|
| Type checking | ✓ (primary) | ✗ (not a type checker) |
| Style linting | ✗ | ✓ (primary) |
| Speed | Slower | Much faster |
Verdict: Use both! Ruff for linting, mypy/PyRight for type checking.
Combined Toolchain Comparison¶
Old Way (slow, complex):
isort . # Import sorting
black . # Formatting
flake8 . # Linting
pyupgrade . # Syntax modernization
pydocstyle . # Docstring style
bandit . # Security
New Way (fast, simple):
Best Practices¶
✅ Do¶
- Run Ruff on save in your editor for immediate feedback
- Use auto-fix liberally - it's safe for most rules
- Enable rules incrementally - start small, add more over time
- Configure in pyproject.toml - single source of truth
- Enforce in CI - prevent unlined code from merging
- Use per-file ignores instead of inline
# noqacomments - Keep Ruff updated - it improves frequently
- Trust the tool - Ruff's defaults are well-considered
- Run with
--unsafe-fixeslocally for aggressive fixes - Document ignored rules - explain why in comments
❌ Don't¶
- Don't ignore entire rule categories without consideration
- Don't use
# noqaexcessively (prefer config) - Don't skip linting in CI
- Don't mix Ruff with Flake8 (use one or the other)
- Don't enable all rules immediately (gradual adoption)
- Don't ignore Ruff warnings in pre-commit
- Don't manually fix issues that Ruff can auto-fix
- Don't use different line lengths for linting and formatting
Configuration Best Practices¶
Line Length Consistency
Use the same line-length for both linting and formatting:
Explicit Over Implicit
Explicitly list enabled rules instead of relying on defaults:
Avoid Blanket Ignores
Troubleshooting Common Issues¶
Issue: Too Many Violations¶
Symptom: Running ruff check . shows hundreds of errors
Solution: Enable rules gradually
# Start with just errors
ruff check --select=E,F .
# Add rules incrementally
ruff check --select=E,F,I .
ruff check --select=E,F,I,W .
Update config as you fix violations:
[tool.ruff.lint]
select = ["E", "F"] # Start here
# Add more as codebase improves
# select = ["E", "F", "I", "W", "N"]
Issue: Import Sorting Conflicts¶
Symptom: Ruff and isort disagree on import order
Solution: Remove isort, use Ruff's built-in import sorting
[tool.ruff.lint]
select = ["I"] # Enable import sorting
[tool.ruff.lint.isort]
known-first-party = ["myproject"]
section-order = [
"future",
"standard-library",
"third-party",
"first-party",
"local-folder"
]
Issue: Specific Rule Too Strict¶
Symptom: One rule causes many false positives
Solution: Ignore that specific rule
[tool.ruff.lint]
ignore = ["E501"] # Line too long
# Or ignore for specific files
[tool.ruff.lint.per-file-ignores]
"tests/*.py" = ["E501"]
Issue: Migrations Keep Getting Flagged¶
Symptom: Django migrations show lint errors
Solution: Exclude migrations entirely
[tool.ruff]
exclude = ["*/migrations/*.py"]
# Or ignore all rules
[tool.ruff.lint.per-file-ignores]
"*/migrations/*.py" = ["ALL"]
Issue: Pre-commit Hook Fails¶
Symptom: pre-commit run fails with Ruff errors
Solution: Run Ruff locally first
# See what's wrong
ruff check .
# Auto-fix issues
ruff check --fix .
# Try pre-commit again
pre-commit run --all-files
Issue: CI Fails, Local Passes¶
Symptom: Ruff passes locally but fails in CI
Solution: Ensure consistent Ruff version
# .pre-commit-config.yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8 # Match pinned version
Issue: Performance Slow on Large Codebase¶
Symptom: Ruff takes longer than expected
Solution: Exclude unnecessary directories
Use .git exclusion patterns:
Issue: Unclear Error Messages¶
Symptom: Don't understand what rule wants
Solution: Use --explain flag
Advanced Configuration¶
Custom Rule Selection¶
[tool.ruff.lint]
# Select specific rules, not entire categories
select = [
"E4", # Import errors only
"E7", # Statement errors only
"F", # All Pyflakes
]
# Fine-tune with specific ignores
ignore = [
"E402", # Module level import not at top
"F401", # Unused import
]
Django-Specific Configuration¶
[tool.ruff.lint]
select = [
"DJ", # flake8-django
]
[tool.ruff.lint.flake8-django]
settings-module = "myproject.settings"
[tool.ruff.lint.per-file-ignores]
# Django patterns
"*/migrations/*.py" = ["ALL"]
"settings/*.py" = ["F403", "F405"]
"**/management/commands/*.py" = ["T201"] # Allow print
Type-Checking Imports¶
[tool.ruff.lint]
select = ["TCH"] # flake8-type-checking
[tool.ruff.lint.flake8-type-checking]
# Move type-checking imports to TYPE_CHECKING block
strict = true
Logging Best Practices¶
[tool.ruff.lint]
select = ["LOG"] # flake8-logging
# Example: Catches lazy logging issues
# Bad: logger.info("User: %s" % user)
# Good: logger.info("User: %s", user)
Custom Dummy Variable Pattern¶
[tool.ruff.lint]
# Allow specific unused variable patterns
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Allows: _, __, _unused, _variable
Integration Examples¶
justfile Commands¶
justfile:
# Lint code
lint:
ruff check .
# Lint and auto-fix
fix:
ruff check --fix .
# Aggressive fix
fix-all:
ruff check --fix --unsafe-fixes .
# Format and lint
fmt:
ruff format .
ruff check --fix .
# Pre-commit check (no changes)
check:
ruff format --check .
ruff check .
# CI-style check
ci: check
pytest --cov
Makefile Commands¶
Makefile:
.PHONY: lint fix fmt check
lint:
ruff check .
fix:
ruff check --fix .
fmt:
ruff format .
ruff check --fix .
check:
ruff format --check .
ruff check .
tox Configuration¶
tox.ini:
[tox]
envlist = lint,py313
[testenv:lint]
deps = ruff
commands =
ruff format --check .
ruff check .
[testenv:fix]
deps = ruff
commands =
ruff format .
ruff check --fix .
Migration Guide¶
From Flake8¶
-
Remove Flake8 dependencies:
-
Install Ruff:
-
Convert configuration:
# New: pyproject.toml
[tool.ruff]
line-length = 180
exclude = [".git", "__pycache__", "migrations"]
[tool.ruff.lint]
ignore = ["E203", "W503"]
- Test equivalence:
From Pylint¶
- Keep or remove Pylint (decision point):
- Remove if: Speed is priority, Ruff rules sufficient
-
Keep if: Need deep analysis, custom checks
-
Convert Pylint rules to Ruff:
# New: pyproject.toml (equivalent Ruff rules)
[tool.ruff.lint]
select = ["PL"] # Pylint category
ignore = [
"PLR0913", # Too many arguments
"PLR2004", # Magic value
]
- Run both initially:
Real-World Examples¶
Example 1: Selective Pre-commit Rules¶
From zenith project:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff
args:
- --unsafe-fixes
- --fix
- --select=T201,T203,W191,W291,W292,W605,LOG001,LOG002,LOG007,LOG009,PLC0206,PLC0208,Q000,Q001,Q002,Q003
Rationale: Only auto-fix safe, non-controversial rules in pre-commit (print statements, whitespace, quotes). Run full checks in CI.
Example 2: Minimal Configuration¶
From zenith project:
[tool.ruff]
exclude = [
".bzr", ".direnv", ".eggs", ".git", ".git-rewrite",
".hg", ".mypy_cache", ".nox", ".pants.d", ".pytype",
".ruff_cache", ".svn", ".tox", ".venv", "__pypackages__",
"_build", "buck-out", "build", "dist", "node_modules", "venv",
]
line-length = 180
target-version = "py313"
[tool.ruff.lint]
ignore = ["F403"]
Rationale: Minimal config. Only ignore F403 (wildcard imports) which is common in Django settings.
Example 3: Django Project Full Config¶
[tool.ruff]
line-length = 180
target-version = "py313"
exclude = [
"migrations",
"venv",
".venv",
"node_modules",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"DJ", # flake8-django
"T20", # flake8-print
"Q", # flake8-quotes
]
ignore = ["F403", "F405"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"settings/*.py" = ["F403", "F405"]
"*/migrations/*.py" = ["ALL"]
"*/management/commands/*.py" = ["T201"]
"tests/*.py" = ["ARG001"]
[tool.ruff.lint.isort]
known-first-party = ["myproject"]
Performance Benchmarks¶
Linting Speed¶
| Codebase Size | Flake8 | Ruff | Improvement |
|---|---|---|---|
| Small (50 files) | 1.2s | 0.01s | 120x faster |
| Medium (500 files) | 12s | 0.10s | 120x faster |
| Large (5000 files) | 120s | 1.0s | 120x faster |
Pre-commit Hook Time¶
| Tool Chain | Time |
|---|---|
| Flake8 + isort + Black | 3.5s |
| Ruff (lint + format) | 0.1s |
Impact: Faster pre-commit = less developer frustration, more likely to be used.
Further Reading¶
Next Steps¶
- Code Formatting with Ruff - Companion guide to formatting
- Pre-commit Hooks - Automate linting in Git workflow
- Testing - Quality through testing
- Security - Security scanning with Bandit and Vulture