FreeCampus Python

Format and Lint a Project with Ruff

Configure Ruff, interpret rule diagnostics, review automatic fixes, and separate deterministic layout from human design and behavior evidence.
python-foundations code-quality-maintainability ruff linting formatting
Open in Colab
  • Level: Python Foundations
  • Estimated time: 3–4 hours
  • You will learn: Run Ruff’s formatter and linter as distinct tools, trace findings to configured rules, and turn a noisy file into a reviewed patch.
  • Practice in: The Midnight Museum local project, terminal, and Git diff

1. Decide which question you are asking

A teammate sends this museum helper:

import os
import json
from pathlib import Path


def load_team(path: Path):
    data = json.loads(path.read_text())
    return data

Before running a tool, ask:

  1. Is the layout consistent with the project?
  2. Does static source contain a suspicious or unused construct?
  3. Do the declared types agree across calls?
  4. Does the function return the correct data for real files and failures?
  5. Is the interface understandable to a future maintainer?

Those are different questions:

  • ruff format rewrites deterministic layout.
  • ruff check reports configured lint rules and can fix selected findings.
  • MyPy compares static type contracts.
  • pytest executes behavioral examples.
  • a human decides whether load_team has a useful name, contract, error policy, and responsibility.

Do not treat a clean result from one as a substitute for the others.

From the project root, run the linter without changing files:

ruff check src tests

For the helper above, Ruff 0.15.20 with the course project’s rules reports:

I001 [*] Import block is un-sorted or un-formatted
 --> src/midnight_museum/storage.py:1:1
help: Organize imports

F401 [*] `os` imported but unused
 --> src/midnight_museum/storage.py:1:8
help: Remove unused import: `os`

Found 2 errors.
[*] 2 fixable with the `--fix` option.

The file is valid Python and might even pass a happy-path test. The report gives a different form of evidence.

2. Read a Ruff finding before fixing it

Each diagnostic tells you:

Field Meaning in the sample
I001 Rule code; I is the import-sorting family
[*] Ruff marks an automatic fix as available
Message The import block is not in configured order
Path and 1:1 File, line, and column where the finding is anchored
Help Proposed action, not proof that the action fits every intention

F401 comes from the Pyflakes-compatible F family. It says the imported name is unused in this module. Usually deleting os is correct. Sometimes an import is deliberately re-exported from a package. In that case, make the public interface explicit—often through __all__—rather than accumulating unexplained ignores.

Run one rule or one file while investigating

Focus shortens the feedback loop:

ruff check src/midnight_museum/storage.py
ruff check src/midnight_museum/storage.py --select F401
ruff rule F401

The first selects a file, the second selects a rule for this run, and the third opens Ruff’s local explanation. Command-line selection is useful for diagnosis; the committed project configuration remains the shared policy.

Common rule families in this course

The repository selects a deliberate, moderate set:

Prefix Kind of evidence Example
E pycodestyle errors ambiguous whitespace or a configured long line
F Pyflakes correctness signals undefined or unused names
I import organization standard-library and local imports out of order
UP syntax compatible with the target Python older typing syntax that can be modernized
B bug-prone patterns a mutable default or loop-variable capture
RUF Ruff-specific checks ambiguous or unsafe source patterns

A rule family is not inherently right for every project. Select it because the team understands and wants its policy.

Checkpoint: classify Ruff evidence

3. Let the formatter own mechanical layout

This function is valid Python but inconsistent with the project:

def museum_label( team:str,score:int )->str:
    return f"{team}:{score}"

Run the formatter in place:

ruff format src/midnight_museum/labels.py

The result is deterministic for the selected Ruff version and configuration:

def museum_label(team: str, score: int) -> str:
    return f"{team}:{score}"

To check without rewriting—appropriate for a gate—use:

ruff format --check src tests

A file needing changes produces a nonzero exit status. A clean run looks like:

3 files already formatted

Review formatting as a diff

Even a deterministic tool can touch many lines. Use Git to see the patch:

git diff -- src tests

Check that only layout changed, then run behavior tests. A formatter aims to preserve behavior, but project confidence comes from the complete evidence chain, not trust in a slogan.

Do not fight the formatter line by line

Ruff’s formatter deliberately has a small configuration surface. Agree on a line length and a few stable preferences rather than hand-arranging every expression. If a formatted expression is difficult to read, improve the source structure—extract an intermediate value or function—instead of adding fragile spacing.

def score_attempt(attempt):
    clue_points = attempt["clues_found"] * 30
    bonus_points = 25 if attempt["bonus_clue"] else 0
    overtime_penalty = max(0, attempt["elapsed_minutes"] - 45) * 2
    return max(0, clue_points + bonus_points - overtime_penalty)

The names solve a reading problem that line wrapping cannot.

4. Apply fixes in an order you can explain

Ruff can apply marked safe fixes:

ruff check src tests --fix
ruff format src tests

For the import sample, the first command removes os and sorts the remaining imports:

import json
from pathlib import Path


def load_team(path: Path):
    data = json.loads(path.read_text())
    return data

Run lint fixes before formatting because a fix may reorganize imports or source that then needs formatting. After both commands:

git diff --check
ruff check src tests
ruff format --check src tests
pytest -q

Safe does not mean semantically proven

A “safe fix” means Ruff classifies it as preserving runtime behavior for the rule’s model. It cannot know external reflection, unusual import side effects, or every dynamic dependency. Review the diff.

Ruff can expose fixes it labels unsafe, but do not enable them as a blanket shortcut:

ruff check src tests --unsafe-fixes

This displays availability; adding --fix --unsafe-fixes would apply them. Use an unsafe fix only after reading the rule, understanding the behavior risk, and having checks that cover the affected contract.

Fix the cause or document a narrow exception

Suppose a pytest fixture intentionally receives an unused conventional argument. A per-file or rule-specific exception can be clearer than renaming values throughout production code. But an unexplained global ignore trains future maintainers to disregard useful output.

Use this decision order:

  1. Is the finding a real bug or unnecessary construct? Fix the source.
  2. Does a safe mechanical fix match the intention? Apply and review it.
  3. Is the rule wrong for the whole project? Change the explicit policy with a reason and review.
  4. Is one file genuinely different? Add the narrowest documented exception.
  5. Never change configuration merely to make a dashboard green.

Checkpoint: formatter and fixer workflow

5. Commit the policy in pyproject.toml

A shared project should not depend on one person’s editor settings. Put the policy beside other project metadata:

[tool.ruff]
line-length = 88
target-version = "py310"
src = ["src", "tests"]

[tool.ruff.lint]
select = [
  "E",
  "F",
  "I",
  "UP",
  "B",
  "RUF",
]

[tool.ruff.format]
quote-style = "double"

Understand each field

  • line-length guides wrapping and relevant lint rules; it is not permission to compress every statement up to character 88.
  • target-version lets Ruff avoid syntax unavailable to the oldest supported Python. It must agree with [project].requires-python.
  • src helps import classification for a src layout.
  • select makes the chosen rule families explicit. Ruff documentation advises adding families deliberately; ALL silently grows when a new release adds rules.
  • formatter options choose a few stable preferences without attempting to encode personal layout for every construct.

Ruff discovers pyproject.toml, ruff.toml, or .ruff.toml by walking from a file toward parent directories. Run from the expected project root and inspect configuration when a result surprises you:

ruff check src/midnight_museum/quest.py --show-settings

File discovery and Git ignores

By default, current Ruff versions discover Python files and Jupyter notebooks and respect common ignore files. Passing a file explicitly can override ordinary exclusion unless force-exclude is configured. This distinction explains why “Ruff skipped the generated directory during a project run” and “Ruff checked a named generated file” can both be true.

Use tool-specific exclusion when lint and format policy differ:

[tool.ruff.format]
exclude = ["generated/*.py"]

Avoid excluding tests merely because they reveal findings. Tests can have narrow, justified differences:

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]

Only add a rule that is actually selected and understood. S101 is shown as a configuration-shape example; it is not selected by this unit’s base policy.

6. Diagnose configuration instead of guessing

Three common surprises have different causes:

“The formatter passed, but lint failed”

Expected. Formatting handles layout; lint rules inspect other source patterns. Run and interpret both.

“My editor and terminal disagree”

Check whether the editor uses the project configuration and the same Ruff version. An editor-specific override can take precedence. Run the committed command in the declared environment as the reproducible result.

“A rule appeared after an upgrade”

An explicit rule family can gain rules in a later tool release. Review the release and new diagnostic, then either repair, configure a reasoned exception, or pin/update the tool deliberately. Do not combine a large tool upgrade with an unrelated refactor if you want a reviewable diff.

Committed configuration and a resolved tool version produce the repeatable Ruff result.

flowchart LR
  A[Python files] --> D[Ruff run]
  B[pyproject policy] --> D
  C[Resolved Ruff version] --> D
  D --> E[Diagnostics]
  D --> F[Formatted diff]
  E --> G[Human review]
  F --> G

Checkpoint: configuration boundaries

7. Lab: turn six findings into a reviewed patch

Create src/midnight_museum/cleanup.py with this valid source:

import os
import json
from pathlib import Path


BONUS = 25


def load_score(path: Path):
    data = json.loads(path.read_text())
    score = data["clues"] * 30
    if data["bonus"] == True:
        score += BONUS
    return score


def labels(names=[]):
    names.append("MIDNIGHT MUSEUM")
    return names

Use the unit configuration, then:

  1. run ruff check without fixes and classify every finding by rule code;
  2. predict which findings --fix will change;
  3. apply safe fixes, then run ruff format;
  4. inspect the diff before editing remaining findings;
  5. replace the mutable default with None and create a fresh list;
  6. replace the explicit == True comparison with the boolean condition;
  7. add useful parameter/return annotations without turning this into the MyPy lesson again;
  8. run lint, format check, MyPy, and pytest; and
  9. explain one choice Ruff made and one design choice you made.
Hint A: start with imports

os is unused and the import block is out of order. Ruff can remove and sort those safely. Review the resulting import group.

Hint B: repair the mutable default

Use def labels(names: list[str] | None = None) -> list[str]:, then create result = [] if names is None else list(names). Returning a copy also avoids mutating a caller’s supplied list.

Hint C: finish with check-only commands

After edits, run ruff check src tests, ruff format --check src tests, mypy src, and pytest -q. The final gate should not rewrite anything.

Show one reviewed result
import json
from pathlib import Path

BONUS_POINTS = 25


def load_score(path: Path) -> int:
    data = json.loads(path.read_text())
    score = data["clues"] * 30
    if data["bonus"]:
        score += BONUS_POINTS
    return score


def labels(names: list[str] | None = None) -> list[str]:
    result = [] if names is None else list(names)
    result.append("MIDNIGHT MUSEUM")
    return result

The automatic pass can remove/sort imports and normalize layout. A maintainer chooses the name BONUS_POINTS, the mutable-default repair, copy behavior, and boundary annotations. Add runtime JSON validation in the appropriate boundary lesson/project rather than claiming the decoded object is statically safe.

Key points

  • Formatter, linter, type checker, tests, and human review answer different questions.
  • Read path, position, rule code, message, and fix status before changing code.
  • Apply lint fixes before formatting, inspect the diff, and rerun check-only commands.
  • Safe fixes are still reviewed changes; unsafe fixes require explicit behavior reasoning.
  • Commit a small understood policy and a compatible tool version instead of relying on editor state.
  • A narrow justified exception is better than a global ignore, but repairing the source is usually the first choice.

References

Continue to Lesson 5: Catch Small Problems Before Commit

Back to top