How to Set Up Ruff Python Linter and Formatter in Your Project
I was frustrated with my Python project’s linting setup. Every time I ran the pre-commit hooks, I had to wait for Flake8, Black, isort, and pyupgrade to run sequentially. The feedback loop was slow, and configuring each tool separately was a headache.
Then I discovered Ruff.
The Problem with Traditional Python Linting
My .pre-commit-config.yaml looked like this:
repos: - repo: https://github.com/psf/black rev: 23.12.1 hooks: - id: black - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade rev: v3.15.0 hooks: - id: pyupgradeFour different tools. Four different configurations. Four different execution times. On a medium-sized project, this took over 30 seconds on every commit.
And that’s not counting the dependencies each tool brought along. My development environment was bloated with packages I only used for code quality checks.
I knew there had to be a better way.
Finding Ruff
Ruff caught my attention when I saw it mentioned in the FastAPI repository. The claims were bold: “10-100x faster than existing linters.”
I was skeptical. How could one tool replace four? And be 100x faster?
The answer lies in its implementation. Ruff is written in Rust, not Python. This gives it native performance that Python-based tools simply cannot match. It also uses a single AST pass to analyze code, whereas traditional tools often parse files multiple times.
But speed means nothing if the tool doesn’t do what I need.
Installing Ruff in a New Project
I decided to test Ruff with a fresh project. Here’s how I set it up:
$ uv init --lib myprojectInitialized project `myproject` at `/Users/zhaocaiwen/projects/myproject`The uv tool is a fast Python package manager, also from Astral (the same company behind Ruff). If you don’t have uv, you can use pip instead:
$ pip install ruffBut I prefer uv for its speed and modern dependency management. Adding Ruff as a development dependency:
$ uv add --dev ruffUsing CPython 3.12.0Adding ruff to dev dependenciesThat’s it. No additional configuration files. No plugin installation. Ruff works out of the box.
Running My First Check
Let me see what Ruff finds in my code. I created a simple module with an intentional error:
import os
def calculate_sum(a: int, b: int) -> int: return a + bRunning the linter:
$ uv run ruff checksrc/myproject/calculate.py:3:8: F401 [*] `os` imported but unusedFound 1 error.[*] 1 fixable with the `--fix` option.Ruff immediately caught the unused import. Notice the [*] indicator - this means Ruff can automatically fix the issue.
Auto-fixing Issues
I don’t want to manually remove imports every time. Let Ruff handle it:
$ uv run ruff check --fixFound 1 error (1 fixed, 0 remaining).One command, one fix. The --fix flag is incredibly useful during development. You can run it on save or as a pre-commit hook.
After the fix, my code looked clean:
def calculate_sum(a: int, b: int) -> int: return a + bFormatting Code
Linting catches errors, but what about code style? That’s where Ruff’s formatter comes in.
I wrote some poorly formatted code:
def calculate_sum(a:int,b:int)->int: return a+ b
def process_data( data , options ): result=[] for item in data: if item>0:result.append(item*2) return resultRunning the formatter:
$ uv run ruff format1 file reformattedRuff formatted my code to match Black’s style:
def calculate_sum(a: int, b: int) -> int: return a + b
def process_data(data, options): result = [] for item in data: if item > 0: result.append(item * 2) return resultNotice the changes:
- Proper spacing around operators
- Consistent function parameter spacing
- Expanded one-liner
ifstatement - Proper blank lines between functions
This is Black-compatible formatting, but running in milliseconds instead of seconds.
Understanding the Speed Improvement
Why is Ruff so fast? I investigated.
Ruff is written in Rust, compiled to native code. Traditional Python linters like Flake8 are, well, written in Python. They’re interpreted at runtime, which adds overhead.
But the real speed boost comes from Ruff’s architecture:
Single AST Pass: Ruff parses your code once into an Abstract Syntax Tree (AST), then runs all rules against that single representation. Flake8 and similar tools often parse the same file multiple times for different checks.
Built-in Caching: Ruff tracks which files have changed and only re-analyzes those. On subsequent runs, it’s nearly instantaneous.
Parallel Processing: Ruff uses all available CPU cores to analyze multiple files simultaneously. Many Python linters are single-threaded.
On a project with 1,000 Python files:
- Flake8 + Black + isort: ~45 seconds
- Ruff check + format: ~0.8 seconds
That’s a 56x improvement. On larger codebases like Pandas or FastAPI, the difference is even more dramatic.
Configuring Ruff (When You Need To)
Ruff’s defaults work well for most projects. But if you need customization, create a ruff.toml or add to pyproject.toml:
[tool.ruff]line-length = 88target-version = "py312"
[tool.ruff.lint]select = ["E", "F", "I", "N", "W", "UP"]ignore = ["E501"]
[tool.ruff.format]quote-style = "double"indent-style = "space"The select option lets you choose which rule categories to enable:
E- pycodestyle errorsF- PyflakesI- isortN- pep8-namingW- pycodestyle warningsUP- pyupgrade
Ruff has over 800 built-in rules, covering most Flake8 plugins and more. Check the full list with:
$ ruff rule --allCommon Mistakes I Made
Running multiple linters alongside Ruff
When I first adopted Ruff, I kept Black and isort in my pre-commit hooks “just in case.” This caused conflicts - Ruff would format code one way, Black would reformat it slightly differently. I finally removed the redundant tools and trusted Ruff entirely.
Not using —fix enough
I used to manually correct every lint error Ruff reported. Then I realized --fix handles 90% of issues automatically. Now I run ruff check --fix before every commit.
Over-configuring
My initial Ruff configuration had 50 lines of rule tweaks. After a month, I realized I didn’t need most of them. Ruff’s defaults follow established Python conventions. I trimmed my config to 10 lines.
Integration with Pre-commit Hooks
To run Ruff automatically before commits, add to .pre-commit-config.yaml:
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.3.0 hooks: - id: ruff args: [--fix] - id: ruff-formatThis replaces my earlier 20-line configuration with 6 lines. And it runs 50x faster.
What Ruff Replaces
Ruff consolidates these tools into one:
| Tool | Purpose | Ruff Equivalent |
|---|---|---|
| Flake8 | Linting | ruff check |
| Black | Formatting | ruff format |
| isort | Import sorting | Built into ruff check |
| pyupgrade | Modernize syntax | Built into ruff check |
| pydocstyle | Docstring checking | Built into ruff check |
| autoflake | Remove unused imports | ruff check --fix |
Not all Flake8 plugins are supported, but the most common ones are covered. Check Ruff’s compatibility chart for specifics.
When Ruff Might Not Be Right
Ruff isn’t perfect for every situation:
Complex custom Flake8 plugins: If you rely on niche Flake8 plugins that Ruff doesn’t support, you’ll need to keep Flake8 alongside Ruff or port the plugin logic.
Large monorepos with mixed Python versions: Ruff’s target version detection can sometimes miss edge cases in complex version matrices.
CI/CD with specific tool requirements: Some CI pipelines expect specific linter output formats. Ruff is mostly compatible but has minor differences.
For most projects, these edge cases won’t matter. But be aware of them when migrating.
Summary
Ruff transformed my Python development workflow. One tool replaces four. Speed improved 50x. Configuration simplified from 50 lines to 10.
The setup is trivial:
$ uv add --dev ruff$ uv run ruff check --fix$ uv run ruff formatThat’s it. No plugins, no complex configuration, no waiting.
If you’re still using separate linting and formatting tools, give Ruff a try. The speed difference alone is worth it, and the unified tooling is a bonus.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Ruff: An extremely fast Python linter and formatter
- 👨💻 Astral: Allocating our speed allowance
- 👨💻 astral-sh/ruff: An extremely fast Python linter and code formatter
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments