Skip to content

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

# With uv (recommended)
uv pip install ruff

# Or add to requirements/dev.in
ruff>=0.11.0

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.

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"]
# 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

[tool.ruff.lint.per-file-ignores]
"scripts/*.py" = ["T201"]
"manage.py" = ["T201"]

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

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/conftest.py" = ["F401"]

Django Migrations (ALL)

# Generated code shouldn't be linted
# migrations/0001_initial.py

Always ignore: All migration files

[tool.ruff.lint.per-file-ignores]
"*/migrations/*.py" = ["ALL"]

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:

  1. Open Extensions (Ctrl+Shift+X)
  2. Search for "Ruff"
  3. 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

  1. Install Ruff plugin:
  2. Settings → Plugins
  3. Search "Ruff"
  4. Install and restart

  5. Configure Ruff:

  6. Settings → Tools → Ruff
  7. Enable "Run Ruff on save"
  8. Enable "Show Ruff warnings"

  9. External Tool (Alternative):

  10. Settings → Tools → External Tools
  11. 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:

(use-package flycheck-ruff
  :ensure t
  :hook (python-mode . flycheck-mode))

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)

- uses: chartboost/ruff-action@v1
  with:
    args: check --output-format=github

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):

ruff check --fix .  # Linting + auto-fix
ruff format .       # Formatting

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 # noqa comments
  • Keep Ruff updated - it improves frequently
  • Trust the tool - Ruff's defaults are well-considered
  • Run with --unsafe-fixes locally for aggressive fixes
  • Document ignored rules - explain why in comments

❌ Don't

  • Don't ignore entire rule categories without consideration
  • Don't use # noqa excessively (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:

[tool.ruff]
line-length = 180  # Shared by lint and format

Explicit Over Implicit

Explicitly list enabled rules instead of relying on defaults:

select = ["E", "F", "I", "W", "N"]  # Clear and intentional

Avoid Blanket Ignores

# ❌ Bad: Ignores too much
ignore = ["E", "W"]

# ✅ Good: Specific ignores
ignore = ["E501", "W503"]

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

# pyproject.toml
[project]
dependencies = [
    "ruff==0.11.8",  # Pin exact 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

[tool.ruff]
exclude = [
    ".venv",
    "node_modules",
    "build",
    "dist",
    "*.egg-info",
]

Use .git exclusion patterns:

[tool.ruff]
respect-gitignore = true  # Default, but explicit

Issue: Unclear Error Messages

Symptom: Don't understand what rule wants

Solution: Use --explain flag

# Get detailed explanation
ruff check --explain E501

# Or look up rule online
ruff rule E501

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

  1. Remove Flake8 dependencies:

    # Remove from requirements
    pip uninstall flake8 flake8-*
    
    # Remove from pre-commit
    # Delete flake8 hook from .pre-commit-config.yaml
    

  2. Install Ruff:

    uv pip install ruff
    

  3. Convert configuration:

    # Old: .flake8 or setup.cfg
    [flake8]
    max-line-length = 180
    ignore = E203, W503
    exclude = .git, __pycache__, migrations
    

# New: pyproject.toml
[tool.ruff]
line-length = 180
exclude = [".git", "__pycache__", "migrations"]

[tool.ruff.lint]
ignore = ["E203", "W503"]
  1. Test equivalence:
    # Compare outputs
    flake8 . > flake8.txt
    ruff check . > ruff.txt
    diff flake8.txt ruff.txt
    

From Pylint

  1. Keep or remove Pylint (decision point):
  2. Remove if: Speed is priority, Ruff rules sufficient
  3. Keep if: Need deep analysis, custom checks

  4. Convert Pylint rules to Ruff:

    # Old: .pylintrc
    [MESSAGES CONTROL]
    disable = C0111, C0103, W0212
    

# New: pyproject.toml (equivalent Ruff rules)
[tool.ruff.lint]
select = ["PL"]  # Pylint category
ignore = [
    "PLR0913",  # Too many arguments
    "PLR2004",  # Magic value
]
  1. Run both initially:
    # Compare
    pylint mymodule
    ruff check mymodule --select=PL
    

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