Skip to content

How to Configure Ruff for Python Linting and Formatting

My Python project had five configuration files for code quality: .flake8, pyproject.toml for black, .isort.cfg, .pre-commit-config.yaml, and a setup.cfg for various tool settings. Each tool had its own quirks. Running all of them took 30 seconds on my modest codebase. When I switched to ruff, I deleted four files and cut linting time to under a second.

Ruff consolidates black, flake8, isort, and over 20 other Python tools into one Rust-based tool. Let me show you how to configure it properly.

Why I Switched to Ruff

The main reason is speed. Ruff is 10-100x faster than traditional Python tools because it’s written in Rust. On my project with 500 Python files:

benchmark.txt
black: 12 seconds
flake8: 8 seconds
isort: 5 seconds
Total: 25 seconds
ruff check + ruff format: 0.3 seconds

That feedback loop matters when you’re running linting hundreds of times per day.

The second reason is consolidation. Instead of learning different configuration formats for each tool, I now have one [tool.ruff] section in pyproject.toml. One tool to install, one tool to update, one tool to configure.

What Ruff Replaces

Here’s a quick comparison of what ruff replaces:

ToolPurposeRuff Equivalent
blackCode formattingruff format
flake8Lintingruff check
isortImport sortingRule I in ruff
pydocstyleDocstring checksRule D in ruff
pyupgradeModernize syntaxRule UP in ruff
banditSecurity checksRule S in ruff

Ruff implements over 700 lint rules from dozens of tools. You can check the full list with ruff rule --all.

Installing Ruff

I use uvx to run ruff without installing it globally:

install.sh
# Run directly with uvx (no installation needed)
uvx ruff check .
# Or install with pip
pip install ruff
# Or with uv
uv add --dev ruff

For this post, I’ll assume you’re using uvx or have ruff installed.

Basic Configuration

Create a pyproject.toml file with ruff configuration. Here’s my starting point:

pyproject.toml
[tool.ruff]
# Target Python version - affects which rules apply
target-version = "py311"
# Same line length as Black (default is 88)
line-length = 88
indent-width = 4
# Directories to exclude
exclude = [
".git",
".venv",
"__pycache__",
"build",
"dist",
"*.egg-info",
]
[tool.ruff.lint]
# Enable basic rule sets
select = [
"E", # pycodestyle errors
"F", # Pyflakes (undefined names, unused imports)
"I", # isort (import sorting)
]
# Rules to ignore
ignore = [
"E501", # line too long (formatter handles this)
]
# Allow autofix for all enabled rules
fixable = ["ALL"]
[tool.ruff.format]
# Match Black's defaults
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

This configuration:

  • Targets Python 3.11
  • Uses 88-character line length (same as Black)
  • Enables basic linting rules (pycodestyle errors, pyflakes, isort)
  • Ignores line-too-long errors (the formatter handles this)
  • Sets formatting to match Black’s style

Running Ruff

With configuration in place, run ruff:

lint.sh
# Check for linting issues
uvx ruff check .
# Auto-fix issues
uvx ruff check --fix .
# Format code
uvx ruff format .
# Check formatting without changes
uvx ruff format --check .

I typically run ruff check --fix . followed by ruff format . before each commit.

Choosing Rule Sets

The select option determines which rules ruff applies. I started with ["E", "F", "I"] and expanded as needed. Here are the most useful rule sets:

PrefixSourceWhat It Checks
EpycodestyleStyle errors (indentation, whitespace)
FPyflakesLogical errors (undefined names, unused imports)
IisortImport order and grouping
UPpyupgradeUse modern Python syntax
Bflake8-bugbearCommon bugs and design problems
SIMflake8-simplifyCode simplification opportunities
Sflake8-banditSecurity issues

My current preferred setup for most projects:

pyproject.toml
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # Pyflakes
"I", # isort
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
]
ignore = [
"E501", # line too long
"B008", # function call in argument defaults
]

Understanding Specific Rules

If ruff reports an error code you don’t recognize, look it up:

rule-help.sh
# Explain a specific rule
uvx ruff rule E501
# Show all available rules
uvx ruff rule --all

For example, ruff rule E501 outputs:

rule-output.txt
E501: Line too long
This rule is part of the pycodestyle (E/W) rules and checks for lines
exceeding the configured line length.
The default line length is 88 characters.

Per-File Configuration

Some files need different rules. Tests often use assert, which triggers security rule S101. I exclude that rule for test files:

pyproject.toml
[tool.ruff.lint.per-file-ignores]
# Allow unused imports in __init__.py
"__init__.py" = ["F401"]
# Allow assert in tests
"tests/**/*.py" = ["S101"]
# Allow magic values in tests
"tests/**/*.py" = ["PLR2004"]

The glob patterns match file paths relative to your project root.

Adding Security Rules

For production code, I enable security checks:

pyproject.toml
[tool.ruff.lint]
select = [
"E", "F", "I", "UP", "B", "SIM",
"S", # flake8-bandit (security)
]
# Security rules can be noisy in tests
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "S105", "S106"]

Common security rules:

  • S101: Use of assert detected
  • S105: Possible hardcoded password
  • S106: Possible hardcoded password in function argument
  • S608: Possible SQL injection

IDE Integration

For VSCode, install the Ruff extension and configure it:

.vscode/settings.json
{
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"editor.defaultFormatter": "charliermarsh.ruff"
},
"ruff.lint.args": ["--config=pyproject.toml"]
}

For PyCharm, install the Ruff plugin from the marketplace and enable it in Settings > Tools > Ruff.

Pre-commit Hooks

Add ruff to your pre-commit configuration:

.pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

This runs ruff on every commit, fixing issues automatically.

CI/CD Integration

In GitHub Actions, use ruff with uvx:

.github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Lint with ruff
run: |
uvx ruff check .
uvx ruff format --check .

The uvx ruff format --check . command exits with code 1 if any files need formatting, failing the CI build.

Migrating from Black

Ruff is Black-compatible by default. I switched without any code changes. The formatter produces output that matches Black’s style:

  • 88-character line length
  • Double quotes for strings
  • 4-space indentation
  • Trailing commas in multi-line structures

If you have custom Black settings, translate them to ruff:

pyproject.toml
# Old Black configuration
[tool.black]
line-length = 100
skip-string-normalization = true
# New Ruff configuration
[tool.ruff]
line-length = 100
[tool.ruff.format]
quote-style = "preserve" # equivalent to skip-string-normalization

After switching, run ruff format . once to ensure consistency.

Migrating from Flake8

Flake8 configuration translates directly to ruff. Here’s a common flake8 setup:

.flake8
[flake8]
max-line-length = 88
extend-ignore = E203, E501, W503
exclude = .git, __pycache__, build, dist

Equivalent ruff configuration:

pyproject.toml
[tool.ruff]
line-length = 88
exclude = [".git", "__pycache__", "build", "dist"]
[tool.ruff.lint]
ignore = ["E203", "E501", "W503"]

Note that W503 (line break before binary operator) is not needed in ruff because it follows the opposite convention by default, matching Black’s formatting.

Migrating from isort

Ruff’s import sorting (rule I) works like isort. Enable it with:

pyproject.toml
[tool.ruff.lint]
select = ["I"]

If you have custom isort settings:

pyproject.toml
# Old isort configuration
[tool.isort]
known-first-party = ["myproject"]
line-length = 88
multi-line = 3
# Ruff doesn't need most isort settings
# It uses your line-length automatically
# For known-first-party, use src:
[tool.ruff]
src = ["src", "tests"]

Common Pitfalls

I ran into a few issues during migration:

1. Forgetting to run both check and format

Ruff has two commands: ruff check for linting and ruff format for formatting. I initially only ran ruff check and wondered why my code wasn’t formatted. Run both:

full-lint.sh
uvx ruff check --fix .
uvx ruff format .

2. Not setting target-version

Without target-version, ruff won’t apply pyupgrade rules for modern syntax. Always set it:

pyproject.toml
[tool.ruff]
target-version = "py311"

3. Ignoring E501 without setting line-length

The formatter respects line-length, not the E501 rule. Set line-length even if you ignore E501:

pyproject.toml
[tool.ruff]
line-length = 88
[tool.ruff.lint]
ignore = ["E501"]

4. Not excluding virtual environments

Ruff is fast, but scanning .venv is wasteful. Exclude it:

pyproject.toml
[tool.ruff]
exclude = [".venv", "venv", "env"]

Summary

In this post, I showed you how to configure ruff for Python linting and formatting. Ruff replaces black, flake8, and isort with a single tool that runs 10-100x faster. The key steps are:

  1. Create pyproject.toml with [tool.ruff] configuration
  2. Select rule sets starting with ["E", "F", "I"]
  3. Run ruff check --fix . and ruff format .
  4. Add pre-commit hooks for consistency
  5. Integrate with your IDE and CI/CD

Start with minimal rules and expand as you understand each rule set. The speed improvement alone makes the switch worthwhile.

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