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:
black: 12 secondsflake8: 8 secondsisort: 5 secondsTotal: 25 seconds
ruff check + ruff format: 0.3 secondsThat 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:
| Tool | Purpose | Ruff Equivalent |
|---|---|---|
| black | Code formatting | ruff format |
| flake8 | Linting | ruff check |
| isort | Import sorting | Rule I in ruff |
| pydocstyle | Docstring checks | Rule D in ruff |
| pyupgrade | Modernize syntax | Rule UP in ruff |
| bandit | Security checks | Rule 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:
# Run directly with uvx (no installation needed)uvx ruff check .
# Or install with pippip install ruff
# Or with uvuv add --dev ruffFor 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:
[tool.ruff]# Target Python version - affects which rules applytarget-version = "py311"
# Same line length as Black (default is 88)line-length = 88indent-width = 4
# Directories to excludeexclude = [ ".git", ".venv", "__pycache__", "build", "dist", "*.egg-info",]
[tool.ruff.lint]# Enable basic rule setsselect = [ "E", # pycodestyle errors "F", # Pyflakes (undefined names, unused imports) "I", # isort (import sorting)]
# Rules to ignoreignore = [ "E501", # line too long (formatter handles this)]
# Allow autofix for all enabled rulesfixable = ["ALL"]
[tool.ruff.format]# Match Black's defaultsquote-style = "double"indent-style = "space"skip-magic-trailing-comma = falseline-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:
# Check for linting issuesuvx ruff check .
# Auto-fix issuesuvx ruff check --fix .
# Format codeuvx ruff format .
# Check formatting without changesuvx 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:
| Prefix | Source | What It Checks |
|---|---|---|
| E | pycodestyle | Style errors (indentation, whitespace) |
| F | Pyflakes | Logical errors (undefined names, unused imports) |
| I | isort | Import order and grouping |
| UP | pyupgrade | Use modern Python syntax |
| B | flake8-bugbear | Common bugs and design problems |
| SIM | flake8-simplify | Code simplification opportunities |
| S | flake8-bandit | Security issues |
My current preferred setup for most projects:
[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:
# Explain a specific ruleuvx ruff rule E501
# Show all available rulesuvx ruff rule --allFor example, ruff rule E501 outputs:
E501: Line too long
This rule is part of the pycodestyle (E/W) rules and checks for linesexceeding 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:
[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:
[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 ofassertdetectedS105: Possible hardcoded passwordS106: Possible hardcoded password in function argumentS608: Possible SQL injection
IDE Integration
For VSCode, install the Ruff extension and configure it:
{ "[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:
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.0 hooks: - id: ruff args: [--fix] - id: ruff-formatThis runs ruff on every commit, fixing issues automatically.
CI/CD Integration
In GitHub Actions, use ruff with uvx:
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:
# Old Black configuration[tool.black]line-length = 100skip-string-normalization = true
# New Ruff configuration[tool.ruff]line-length = 100
[tool.ruff.format]quote-style = "preserve" # equivalent to skip-string-normalizationAfter switching, run ruff format . once to ensure consistency.
Migrating from Flake8
Flake8 configuration translates directly to ruff. Here’s a common flake8 setup:
[flake8]max-line-length = 88extend-ignore = E203, E501, W503exclude = .git, __pycache__, build, distEquivalent ruff configuration:
[tool.ruff]line-length = 88exclude = [".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:
[tool.ruff.lint]select = ["I"]If you have custom isort settings:
# Old isort configuration[tool.isort]known-first-party = ["myproject"]line-length = 88multi-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:
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:
[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:
[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:
[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:
- Create
pyproject.tomlwith[tool.ruff]configuration - Select rule sets starting with
["E", "F", "I"] - Run
ruff check --fix .andruff format . - Add pre-commit hooks for consistency
- 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:
- 👨💻 Ruff Documentation
- 👨💻 Ruff Rules
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments