We set up Ruff back in Section 1. This lesson covers what it actually does and how to tune it.
Two jobs, one tool
Ruff does two things:
- Formatting — rewriting your code to a consistent style.
- Linting — checking your code for bugs and bad patterns.
Before Ruff, those were separate tools (Black and Flake8 / Pylint). Ruff replaces both and runs about 100× faster.
The basic commands
From inside your project:
ruff format # format every Python file in place
ruff check # report problems
ruff check --fix # auto-fix what it can
If you set up format-on-save in your editor, you’ll rarely run format manually. check is what you’ll run before committing.
Configuration in pyproject.toml
Ruff reads its config from pyproject.toml. A reasonable starting setup:
[tool.ruff]
line-length = 100
target-version = "py313"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes (likely bugs)
"I", # isort (import order)
"B", # bugbear (common bug patterns)
"UP", # pyupgrade (modernise old syntax)
"SIM", # simplify
"RET", # return-related lints
"N", # naming conventions
]
ignore = []
What each group catches:
E/W— basic PEP 8 style (spaces, indentation, line length).F— unused imports, unused variables, undefined names.I— import order. Auto-fixed byruff check --fix.B— patterns that often hide bugs (mutable default args,except:without a type).UP— suggest newer syntax (X | Noneinstead ofOptional[X],list[int]instead ofList[int]).SIM— code that can be simplified.RET— patterns around return statements.N— variable, function, and class naming conventions.
A typical fix-up session
Write some sloppy code:
from typing import Optional, List
import os, sys
def addOne(x:int)->int:
return x+1
def myFunction(items:Optional[List[int]] = None):
if items==None:
items=[]
for i in range(len(items)):
print(items[i])
Run Ruff:
ruff check --fix main.py
ruff format main.py
You’ll get back:
import os
import sys
def add_one(x: int) -> int:
return x + 1
def my_function(items: list[int] | None = None) -> None:
if items is None:
items = []
for item in items:
print(item)
Auto-fixed by Ruff:
- Removed unused imports (
Optional,List). - Sorted imports.
- Fixed spacing.
- Replaced
Optional[List[int]]withlist[int] | None. - Replaced
== Nonewithis None.
Flagged by Ruff for you to fix manually:
addOneandmyFunctionviolate the naming convention (renamed toadd_oneandmy_function).for i in range(len(items)):is unidiomatic (rewritten asfor item in items:).- Missing return type annotation (added
-> None).
Either way, your code is more idiomatic in 30 seconds than it would have been after an hour of manual review.
Ignoring rules
Sometimes a rule doesn’t fit your situation. Two ways to silence:
# silence a specific rule on a specific line
x = some_value # noqa: F841
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["B", "N802"] # skip these rules in test files
Use sparingly. Each ignore is a small bit of cruft. Better to fix the issue when possible.
Pre-commit hook
To never commit unformatted code, install Ruff as a pre-commit hook. Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Then:
uv tool install pre-commit
pre-commit install
From now on, every git commit runs Ruff first. If anything fails, the commit is blocked until you fix it.
This is how professional Python projects keep their style consistent across many contributors.
What’s next
Last lesson of this section — a quick look at PEP 8, the style conventions Ruff enforces.