Skip to content

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:

Multiple linters configuration
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: pyupgrade

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

Create new project with uv
$ uv init --lib myproject
Initialized 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:

Install Ruff with pip
$ pip install ruff

But I prefer uv for its speed and modern dependency management. Adding Ruff as a development dependency:

Add Ruff as dev dependency
$ uv add --dev ruff
Using CPython 3.12.0
Adding ruff to dev dependencies

That’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:

calculate.py with unused import
import os
def calculate_sum(a: int, b: int) -> int:
return a + b

Running the linter:

Run Ruff check
$ uv run ruff check
src/myproject/calculate.py:3:8: F401 [*] `os` imported but unused
Found 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:

Auto-fix with Ruff
$ uv run ruff check --fix
Found 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:

calculate.py after fix
def calculate_sum(a: int, b: int) -> int:
return a + b

Formatting Code

Linting catches errors, but what about code style? That’s where Ruff’s formatter comes in.

I wrote some poorly formatted code:

Messy code before formatting
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 result

Running the formatter:

Format code with Ruff
$ uv run ruff format
1 file reformatted

Ruff formatted my code to match Black’s style:

Code after Ruff format
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 result

Notice the changes:

  • Proper spacing around operators
  • Consistent function parameter spacing
  • Expanded one-liner if statement
  • 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:

pyproject.toml configuration
[tool.ruff]
line-length = 88
target-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 errors
  • F - Pyflakes
  • I - isort
  • N - pep8-naming
  • W - pycodestyle warnings
  • UP - pyupgrade

Ruff has over 800 built-in rules, covering most Flake8 plugins and more. Check the full list with:

List all Ruff rules
$ ruff rule --all

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

Ruff pre-commit configuration
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

This replaces my earlier 20-line configuration with 6 lines. And it runs 50x faster.

What Ruff Replaces

Ruff consolidates these tools into one:

ToolPurposeRuff Equivalent
Flake8Lintingruff check
BlackFormattingruff format
isortImport sortingBuilt into ruff check
pyupgradeModernize syntaxBuilt into ruff check
pydocstyleDocstring checkingBuilt into ruff check
autoflakeRemove unused importsruff 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:

Complete Ruff setup
$ uv add --dev ruff
$ uv run ruff check --fix
$ uv run ruff format

That’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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments