Skip to content
← All Python articles

Python explainer

Decide Which Ruff Rules Should Fail CI

About 4 min to read. Allow extra time to work through examples.

Some Ruff findings are bugs, and some depend on where the code lives. Pick the rules that fail CI, scope exceptions to the files that need them, and upgrade Ruff on purpose.

This article uses Python. You can make it your practice mode to browse related content across the site.

You add Ruff's security rules to your project and run ruff check. Most of the findings are S101, which flags every assert statement. Nearly all of them are in your tests, where assert is how pytest checks a result. One is in your application code:

def apply_discount(price: float, percent: float) -> float:
    assert 0 <= percent <= 100, "percent out of range"
    return price * (1 - percent / 100)

That one is a real bug. Python skips every assert when you run it with the -O flag, so apply_discount(10.0, 150) returns -5.0 instead of failing. Raise an exception for a check that must always run:

def apply_discount(price: float, percent: float) -> float:
    if not 0 <= percent <= 100:
        raise ValueError(f"percent out of range: {percent}")
    return price * (1 - percent / 100)

So the same rule, S101, flags noise in your tests and a bug in your application code. This site grades exercises with their tests and mypy, and never runs Ruff on them. The advice here is for your own projects. The earlier Ruff configuration article covers the setup.

Fail CI on rules you would always fix

A rule belongs in CI when you would make the change for nearly every finding. F and E722 meet that standard. F flags undefined names and unused imports. E722, part of E7, flags a bare except:, which also catches KeyboardInterrupt. A select list replaces Ruff's default rules, so name every family you want. Add other rules by the same standard:

[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "B", "G004", "S101", "T20"]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"]
"src/myapp/cli/**/*.py" = ["T20"]
  • B flags bug patterns such as a mutable default argument, which every call to the function shares.
  • G004 flags an f-string in a logging call. With logger.debug("rows: %s", rows), logging formats the message only if debug logging is on. An f-string formats it on every call.
  • T20 flags print calls. T20 and S101 apply in some files and not in others. The per-file-ignores table turns each one off for the paths where it does not apply, and keeps it everywhere else.

Run judgment rules as advice

Some rules flag code that is often fine. If they fail CI, your team learns to add # noqa comments without reading the finding.

  • PLR0913 flags a function with more than five arguments. Sometimes the fix is to group related values in a dataclass. Sometimes the function needs them all.
  • The S rules for subprocess flag calls that start another program. Each finding asks you to check where the arguments come from. It does not mean the call is unsafe.
  • C901 flags a function whose complexity score is over a limit you set. The next article explains what that score measures.

Ruff has no warning level, so any finding from a selected rule fails ruff check. Leave judgment rules out of select, and run them in a separate step that always passes:

ruff check --select PLR0913,C901 --exit-zero
Check your understanding Five files in src/myapp/cli/ each have # noqa: T201 comments on their print calls. What should you change? Show answer

Move the exception into the configuration with a per-file-ignores entry for src/myapp/cli/**/*.py. Printing is the job of a command-line tool, so the rule never applies there. One configuration entry now says so, instead of a comment on every call. Keep a # noqa: CODE comment for one line that breaks a rule the rest of its file follows, and add a short comment that says why.

Upgrade Ruff on purpose

Pin the Ruff version, and use the same version locally and in CI. When you select a family such as B, you also get the new rules Ruff adds to it in later releases. So read the release notes when you upgrade, and run the checks even if your configuration did not change.

Going further

Ruff marks some automatic fixes as unsafe, because they can change what the code does. ruff check --fix applies only the safe ones unless you opt in. Review the diff either way. Read more about fix safety and the security rules.

Before version 0.16, Ruff's default rules were E4, E7, E9 and F. Since 0.16, about 400 rules are on by default. Run ruff check --show-settings to list the rules your configuration turns on. See the default rules.

step 35 of 41

Modules, Packaging and Tooling

Up next Clean Up Old noqa Comments