Can Ruff Replace Pylint and Black?

Real performance numbers, migration paths, and honest tradeoffs.




Step 1: The Problem With Your Current Setup


Most Python projects reach a point where the CI lint step takes longer than the tests. You run black ., wait. Then pylint src/, wait longer. On a 50k-line codebase, that combo regularly costs 30–60 seconds per run.


$ time black . && pylint src/
reformatted src/utils.py
reformatted src/models/user.py
All done! ✨ 🍰 ✨
2 files reformatted, 43 files left unchanged.

--------------------------------------------------------------------
Your code has been rated at 8.94/10 (previous run: 8.91/10)

real    0m47.312s


That 47-second wait is the exact friction that makes developers skip linting locally and let CI catch it instead — slowing the whole team down.


Step 2: What Ruff Actually Does


Ruff is a Python linter and formatter written in Rust. It implements rules from Pyflakes, pycodestyle, isort, pydocstyle, flake8, and a large subset of Pylint — all in one binary. It also ships a formatter that is intentionally Black-compatible.


The same 50k-line project:

$ time ruff check . && ruff format .
All checks passed!
2 files reformatted, 43 files left unchanged.

real    0m0.631s


Ruff avoids Python interpreter startup cost, runs checks in parallel, and caches results between runs. The speedup typically lands between 10x and 100x.


Step 3: Installing Ruff


# Global install — use pipx to keep it isolated
$ pipx install ruff

# Or as a dev dependency
$ pip install ruff

# Verify
$ ruff --version
ruff 0.4.4


No compiler, no build step. Pre-built binaries ship for macOS (arm64 and x86_64), Linux, and Windows.


Step 4: Running Your First Check


# Lint the entire project
$ ruff check .

# Auto-fix safe issues
$ ruff check --fix .

# Format (replaces Black)
$ ruff format .

# Check formatting without writing (useful in CI)
$ ruff format --check .


Step 5: Configuring Ruff


Ruff reads from pyproject.toml, ruff.toml, or .ruff.toml.


# pyproject.toml

[tool.ruff]
target-version = "py311"
line-length = 88  # matches Black's default
exclude = [".git", "__pycache__", ".venv", "dist"]

[tool.ruff.lint]
# E/W = pycodestyle, F = Pyflakes, I = isort,
# N = pep8-naming, PL = Pylint, UP = pyupgrade, B = bugbear
select = ["E", "W", "F", "I", "N", "PL", "UP", "B"]
ignore = ["E501"]  # line-too-long (let the formatter handle it)
fixable = ["ALL"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"


The PL prefix maps to Pylint rules. Run ruff rule PLR0913 to see any rule's explanation inline.


Step 6: What Ruff Catches (Concrete Example)


# bad_code.py — intentionally broken
import os
import sys
import json  # unused import

def process_data(data,x,y,z,a,b,c,d,e,f):  # too many args
    if data == None:          # should use `is None`
        return
    result = []
    for i in range(len(data)):  # unnecessary index loop
        result.append(data[i])
    return result
$ ruff check bad_code.py
bad_code.py:3:8: F401 [*] `json` imported but unused
bad_code.py:5:1: E231 Missing whitespace after ','
bad_code.py:5:1: PLR0913 Too many arguments in function definition (10 > 5)
bad_code.py:6:8: E711 [*] Comparison to `None` (use `is` or `is not`)
bad_code.py:9:14: B007 [*] Loop control variable `i` not used in loop body
Found 5 errors.
[*] 3 fixable with the `--fix` option.


After ruff check --fix and a quick manual cleanup:

# clean_code.py
import os
import sys

def process_data(data: list) -> list:
    if data is None:
        return []
    return list(data)


Step 7: Migrating From Black + Pylint


Remove Black:

$ pip uninstall black
# Remove [tool.black] from pyproject.toml
$ ruff format .  # Black-compatible output


Replace pre-commit hooks:

# BEFORE
repos:
  - repo: https://github.com/psf/black
    rev: 24.3.0
    hooks:
      - id: black
  - repo: https://github.com/pylint-dev/pylint
    rev: v3.1.0
    hooks:
      - id: pylint
# AFTER
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format


GitHub Actions:

- name: Lint with Ruff
  run: |
    pip install ruff
    ruff check .
    ruff format --check .


Step 8: Honest Comparison


Feature Pylint + Black Ruff
Speed (50k-line project) ~40–60s ~0.5–1s
Rule count ~500 (Pylint) ~700+ (multi-tool)
Black formatter parity Exact Near-exact (99%+)
Type-aware checks Yes (Pylint) No — use mypy separately
Auto-fix support Limited Extensive
Import sorting Separate tool Built-in (I rules)
Incremental cache No Yes
Editor LSP Mature Mature (ruff-lsp)


The one real gap: Ruff does not perform type-aware analysis. Keep mypy or pyright alongside it for that coverage.


Additional Tips


noqa comments still work — existing inline suppressions carry over with no changes.


result = some_long_function()  # noqa: E501


Per-file ignores:

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["PLR2004", "S101"]
"scripts/migrate_*.py" = ["BLE001"]


Preview formatter changes before switching:

$ ruff format --diff .


VSCode integration — install the Ruff extension (charliermarsh.ruff), then:

{
  "editor.defaultFormatter": "charliermarsh.ruff",
  "editor.formatOnSave": true
}


For most Python projects with active CI or pre-commit hooks, Ruff is the right move. You lose type-aware lint and a handful of obscure Pylint rules, but you get a workflow fast enough that developers will actually run it consistently.