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 by ruff check --fix.
  • B — patterns that often hide bugs (mutable default args, except: without a type).
  • UP — suggest newer syntax (X | None instead of Optional[X], list[int] instead of List[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]] with list[int] | None.
  • Replaced == None with is None.

Flagged by Ruff for you to fix manually:

  • addOne and myFunction violate the naming convention (renamed to add_one and my_function).
  • for i in range(len(items)): is unidiomatic (rewritten as for 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.

Toggle theme (T)