Ruff: The Fastest Python Linter
When it comes to keeping Python code clean, fast, and error‑free, the linter you choose can make a huge difference. Ruff has emerged as the fastest Python linter, delivering instant feedback without sacrificing the depth of analysis you’d expect from tools like Flake8 or Pylint. In this article we’ll explore why Ruff is a game‑changer, how to integrate it into everyday workflows, and which real‑world scenarios benefit the most from its speed.
What Sets Ruff Apart?
Ruff is written in Rust, a systems language renowned for its performance and safety guarantees. By compiling to native code, Ruff can parse and analyze Python files at a fraction of the time taken by pure‑Python linters. This speed isn’t just a vanity metric—it translates into tighter feedback loops, especially in large monorepos where traditional linters can stall CI pipelines for minutes.
Beyond raw speed, Ruff bundles over 400 linting rules, many of which are compatible with popular style guides such as PEP 8, Black, and isort. You can enable, disable, or fine‑tune each rule via a concise TOML configuration file, keeping the setup lightweight and easy to version‑control.
Rust‑Powered Parsing
Most Python linters rely on the ast module from the standard library, which incurs interpreter overhead for every file. Ruff bypasses this by using the rustpython-parser crate, which parses source code directly into an abstract syntax tree in memory. The result is a near‑instant analysis that scales linearly with file size, not with interpreter start‑up time.
Because the parser is written in Rust, Ruff also benefits from aggressive compiler optimizations, zero‑cost abstractions, and memory safety guarantees. This combination yields a linter that can run on modest hardware while still handling massive codebases with ease.
Installing and Configuring Ruff
Getting started with Ruff is straightforward. The tool is distributed via pip and also offers a pre‑compiled binary for faster installation on CI runners.
# Install via pip
pip install ruff
# Or download the binary (Linux example)
curl -L https://github.com/astral-sh/ruff/releases/latest/download/ruff-x86_64-unknown-linux-gnu.tar.gz | tar -xz
sudo mv ruff /usr/local/bin/
After installation, create a pyproject.toml at the root of your project. This file tells Ruff which rules to enforce and which to ignore. Below is a minimal yet practical configuration:
[tool.ruff]
# Enable the most common rule sets
select = ["E", "F", "W", "C90", "N", "B", "Q"]
# Exclude generated files and migrations
exclude = ["**/migrations/**", "build/**", "dist/**"]
# Target Python version for type‑aware checks
target-version = "py311"
# Fine‑tune individual rules
[tool.ruff.per-file-ignores]
"tests/**/*.py" = ["S101"] # allow asserts in test files
With the configuration in place, a single command runs Ruff across your entire repository:
ruff check .
Running Ruff in Watch Mode
During active development, you can keep Ruff running in the background, automatically re‑checking files as they change. This mirrors the experience of a linter integrated into an IDE, but works uniformly across all editors.
ruff watch .
The watch mode prints diagnostics to the terminal in real time, letting you fix issues before they even make it to a commit.
Pro tip: Pairruff watchwith a terminal multiplexer liketmuxso you can keep linting output visible while you code in another pane.
Practical Example: Refactoring a Legacy Codebase
Imagine you inherit a 200‑file Django project that mixes tabs and spaces, contains unused imports, and has a handful of functions that silently swallow exceptions. Running a traditional linter takes ~3 minutes, which discourages frequent checks. Ruff can analyze the same codebase in under 10 seconds.
First, run Ruff to generate a report:
ruff check src/ --output-format=json > ruff-report.json
The JSON output makes it easy to script automated fixes. For instance, you can use jq to extract all unused import warnings (code F401) and feed them into autoflake for removal.
jq -r '.[] | select(.code=="F401") | .filename' ruff-report.json \
| xargs -n1 autoflake --remove-all-unused-imports -i
After the automated step, re‑run Ruff to confirm that the warnings have vanished. You’ll notice a dramatic reduction in the number of linting errors, and the entire process takes less than half a minute.
Integrating Ruff with Black
Ruff’s design intentionally aligns with Black’s formatting conventions. By enabling the Q (quote) rule set, Ruff will flag inconsistent string quoting, which Black can then automatically fix. Adding a pre‑commit hook ensures both tools run together on every commit.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.292
hooks:
- id: ruff
args: [--fix]
Now, when a developer runs git commit, Black formats the code first, and Ruff immediately fixes any remaining style violations, leaving a clean commit.
Pro tip: Use Ruff’s --fix flag sparingly on large diff‑heavy PRs; it can rewrite many lines, making code reviews harder. Reserve it for CI or a dedicated “format‑only” step.
Ruff in Continuous Integration
CI pipelines often become bottlenecks when linting is slow. Replacing Flake8 with Ruff can shave off up to 80 % of linting time, freeing up resources for test execution or deployment steps.
Below is a minimal GitHub Actions workflow that runs Ruff in parallel with your test suite:
name: CI
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install Ruff
run: pip install ruff
- name: Run Ruff
run: ruff check . --output-format=github
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest
The --output-format=github flag formats warnings as annotations directly in the pull‑request view, giving reviewers instant visibility into style violations.
Fail‑Fast Strategy
In safety‑critical environments, you may want the pipeline to fail as soon as a single high‑severity issue appears. Ruff categorizes rules by severity, allowing you to enforce a “fail‑fast” policy:
ruff check . --select=E,F --exit-zero=false
Here, only errors (E) and fatal issues (F) cause the job to exit with a non‑zero status, while warnings (W) are reported but do not block the pipeline.
Advanced Use Cases
Ruff’s extensibility goes beyond static linting. By leveraging its type‑aware mode, you can catch subtle bugs that only surface when type information is considered. This requires installing ruff[typing] and enabling the --extend-select=PY flag.
pip install "ruff[typing]"
ruff check src/ --extend-select=PY
In a data‑processing script that mixes pandas and native Python lists, Ruff can flag mismatched DataFrame column accesses that would otherwise raise a KeyError at runtime.
Another powerful feature is Ruff’s ability to act as a code‑fixer. With --fix, Ruff can automatically replace deprecated syntax, such as converting assertEquals to assertEqual in unittest code, or updating old-style string formatting to f‑strings.
# Before
print("User %s has %d points" % (name, points))
# After ruff --fix
print(f"User {name} has {points} points")
Ruff vs. Other Linters: A Quick Comparison
- Speed: Ruff processes ~10 k lines/second; Flake8 ~1 k lines/second.
- Rule Coverage: Over 400 built‑in rules versus ~200 in Flake8.
- Language: Rust (compiled) vs. Python (interpreted).
- Integration: Native pre‑commit hook, VS Code extension, and GitHub Action support.
- Customization: TOML configuration is concise; no need for multiple config files.
While Ruff excels in speed and rule density, some niche checks (e.g., Django‑specific migrations) still rely on specialized plugins that haven’t been ported yet. In such cases, a hybrid approach—running Ruff first, then a targeted linter—offers the best of both worlds.
Pro tip: Run Ruff on the changed files only in PRs: ruff check $(git diff --name-only origin/main...HEAD). This keeps feedback fast and relevant.
Custom Rules and Plugins
Ruff’s core is deliberately minimal, but the community can extend it via plugins written in Rust. If you have a company‑specific naming convention or a proprietary API that needs enforcement, you can author a plugin and publish it to crates.io. The plugin architecture mirrors that of clippy for Rust, providing a familiar development experience for teams already using Rust tooling.
Creating a plugin involves implementing the Rule trait and registering the rule with Ruff’s registry. Here’s a skeleton of a simple plugin that flags any function named foo as a violation:
use ruff_linter::{Rule, Violation};
use ruff_parser::Stmt;
pub struct NoFooFunction;
impl Rule for NoFooFunction {
fn name(&self) -> &'static str {
"no-foo-function"
}
fn check_stmt(&self, stmt: &Stmt, diagnostics: &mut Vec<Violation>) {
if let Stmt::FunctionDef(func) = stmt {
if func.name == "foo" {
diagnostics.push(Violation::new(
self.name(),
"Avoid using generic function name 'foo'",
func.location,
));
}
}
}
}
After compiling the plugin into a shared library, you point Ruff to it via the --plugins CLI flag. This flexibility ensures Ruff can evolve alongside your codebase’s unique requirements.
Conclusion
Ruff delivers a compelling blend of speed, rule coverage, and modern tooling integration, making it an ideal choice for both small scripts and massive enterprise codebases. By leveraging Rust’s performance, a concise TOML configuration, and seamless CI support, developers can enforce high‑quality Python standards without the latency that traditionally hampers linting workflows. Whether you’re refactoring legacy Django apps, tightening CI pipelines, or crafting custom linting rules, Ruff provides the horsepower to keep your code clean, consistent, and ready for production.