# Ruff Rule Codes Explained: What E, W, F, I, UP, S, B, and C90 Mean


Run `ruff check` on a codebase and it answers in codes: `F401`, `E711`, `B006`. The letters are not severity levels, and they are not arbitrary. Each prefix names the linter the rule originally came from.

[Ruff](https://pydevtools.com/handbook/reference/ruff.md) re-implements 968 rules (as of version 0.15), most drawn from older linters in the flake8 ecosystem, and it kept the established codes wherever it could. Learn a handful of prefixes and the wall of codes turns into a map of the tools Ruff replaced.

## How to read a rule code

A rule code is a prefix plus a number. The prefix identifies the source linter; the number identifies the rule within it:

```text
F401
│└─ rule 401 in that linter's catalog
└── F = Pyflakes
```

Prefixes run from one to five characters (`F`, `UP`, `C90`, `ASYNC`), and configuration selects rules by prefix. In `pyproject.toml`, `select = ["E"]` enables every pycodestyle error, `["E7"]` enables only the statement-level checks like `E711`, and `["E711"]` enables that one rule:

```toml
[tool.ruff.lint]
extend-select = [
    "I",   # all isort rules
    "B",   # all flake8-bugbear rules
    "S1",  # only the S1xx flake8-bandit rules
]
```

## Match each prefix to its source tool

The prefixes from this page's title cover the categories new Ruff users meet first:

| Prefix | Source tool | What it catches |
|---|---|---|
| `E`, `W` | [pycodestyle](https://github.com/PyCQA/pycodestyle) | [PEP 8](https://pydevtools.com/handbook/explanation/what-is-pep-8.md) style: indentation, whitespace, comparisons (`E` errors, `W` warnings) |
| `F` | [Pyflakes](https://github.com/PyCQA/pyflakes) | Logical errors: unused imports, undefined names, duplicate arguments |
| `I` | [isort](https://pycqa.github.io/isort/) | Import sorting and grouping |
| `UP` | [pyupgrade](https://github.com/asottile/pyupgrade) | Syntax made obsolete by your target Python version |
| `S` | [flake8-bandit](https://github.com/tylerwince/flake8-bandit) | Security problems: hardcoded passwords, `pickle`, weak hashes |
| `B` | [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) | Likely bugs: mutable default arguments, useless expressions |
| `C90` | [mccabe](https://github.com/PyCQA/mccabe) | Cyclomatic complexity over a threshold (one rule, `C901`) |

These eight prefixes are a fraction of the catalog. Ruff 0.15 organizes its rules into 59 groups, including `SIM` (flake8-simplify) and `PL` ([Pylint](https://pydevtools.com/handbook/reference/pylint.md)). Run `ruff linter` to print the full prefix-to-tool list for your installed version.

## Why does Ruff use other tools' codes?

The prefixes predate Ruff. They are inherited from the [flake8](https://pydevtools.com/handbook/reference/flake8.md) plugin ecosystem, where each tool claimed its own letter namespace:

- pycodestyle defined the `E` and `W` codes. It was originally released as `pep8` and renamed in 2016 at Guido van Rossum's request, to stop people from conflating the tool with the PEP 8 document itself ([pycodestyle issue #466](https://github.com/PyCQA/pycodestyle/issues/466)).
- Pyflakes never had codes of its own. flake8 assigned the `F` prefix to Pyflakes messages when it wrapped the tool ([flake8 error codes](https://flake8.pycqa.org/en/latest/user/error-codes.html)).
- Plugins picked letters on a first-come basis. flake8-bugbear took `B`, so when flake8-bandit ported bandit's checks (whose native IDs also start with B), it shifted them to `S` for security.
- The letter `C` is shared by two plugins: mccabe uses `C9xx` and flake8-comprehensions uses `C4xx`. Ruff lists them under separate selectors, `C90` and `C4`, so each can be enabled on its own (a bare `C` selects both).

Ruff kept these codes for the major categories, so a flake8 user's `select` and `ignore` lists and `# noqa: F401` comments mostly carry over. A few plugins were re-prefixed along the way: flake8-commas' `C812` became `COM812`, and Pylint's codes gained a `PL` prefix. Where a source tool never had codes (isort and pyupgrade rewrite files rather than report numbered violations), Ruff coined the prefixes itself: `I` holds two isort rules, `UP` holds 48 pyupgrade rules. Rules original to Ruff live under `RUF`.

## How do you look up a rule?

Pass any code to `ruff rule` and it prints the rule's documentation in the terminal:

```console
$ ruff rule F401
# unused-import (F401)

Derived from the **Pyflakes** linter.

Fix is sometimes available.

## What it does
Checks for unused imports.
...
```

The same content is in the [Ruff rules index](https://docs.astral.sh/ruff/rules/), which lists all 968 rules grouped by source tool.

## Which categories should you enable?

Out of the box, Ruff enables only the Pyflakes rules (`F`) and a subset of pycodestyle errors (`E4`, `E7`, `E9`). That default catches real bugs like unused imports and comparisons to `None`, but no style, import-order, or security rules until you opt in.

For a curated starting set, see [How to configure recommended Ruff defaults](https://pydevtools.com/handbook/how-to/how-to-configure-recommended-ruff-defaults.md). Two categories from the table have dedicated guides: [How to sort Python imports with Ruff](https://pydevtools.com/handbook/how-to/how-to-sort-python-imports-with-ruff.md) covers `I`, and [How to enable Ruff's security rules](https://pydevtools.com/handbook/how-to/how-to-enable-ruff-security-rules.md) covers `S`.

## Learn More

- [Ruff: a complete guide](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md) covers configuration, editor integration, and migration
- [Set up Ruff for formatting and checking your code](https://pydevtools.com/handbook/tutorial/set-up-ruff-for-formatting-and-checking-your-code.md) walks through a first Ruff setup
- [How to disable Ruff rules for a block of code](https://pydevtools.com/handbook/how-to/how-to-disable-ruff-rules-for-a-block-of-code.md) shows `# noqa` in practice
- [The Ruff Linter](https://docs.astral.sh/ruff/linter/) documents rule selection and the default rule set
