Migrating from flake8 to Ruff for Type Lints

TL;DR — Ruff reimplements most flake8 plugins as built-in rule prefixes, so migrating is mostly a config translation, not a behavior change. Map flake8-annotationsANN, pyupgradeUP, flake8-type-checkingTCH, flake8-bugbearB, list those prefixes in select, port your ignores, and delete .flake8. Ruff is fast and covers the annotation-hygiene lints, but it is not a type checker — keep mypy for that.

A typical typed project runs flake8 with a stack of plugins bolted on: annotation checks, an import-time-only enforcer, an upgrader, and bugbear. Ruff folds all of those into one Rust binary keyed by rule prefix, so the migration is finding which prefix replaces each plugin and moving your select/ignore lists across.

Mapping flake8 plugins to Ruff rule prefixes Four flake8 plugins on the left map to their Ruff rule-prefix equivalents on the right, all handled by one Ruff binary. flake8 plugins Ruff prefixes flake8-annotations pyupgrade flake8-type-checking flake8-bugbear ANN (ANN001, ANN401) UP (UP006, UP007) TCH (TCH001–003) B (B006, B008) one binary, config in [tool.ruff.lint]
Each flake8 plugin becomes a Ruff rule prefix you list in select; the whole stack collapses into one config block.

The plugin-to-prefix map

Four plugins cover most annotation-relevant linting. Their Ruff equivalents:

  • flake8-annotationsANN — missing-annotation checks. ANN001 (missing function argument annotation), ANN201 (missing return annotation), ANN401 (dynamically-typed Any in a signature).
  • pyupgradeUP — modernizers. UP006 (List[int]list[int]), UP007 (Union[X, Y]X | Y), UP035 (deprecated typing imports), UP045 (Optional[X]X | None).
  • flake8-type-checkingTCH — move import-only-for-typing names into an if TYPE_CHECKING: block. TCH001 (first-party), TCH002 (third-party), TCH003 (stdlib).
  • flake8-bugbearB — likely-bug patterns. B006 (mutable default argument), B008 (function call in a default).

Translate the config

Here is a representative .flake8 and its [tool.ruff.lint] equivalent in pyproject.toml.

# .flake8 — before
[flake8]
max-line-length = 100
select = E,F,W,ANN,B
extend-select = TCH
ignore = ANN101,ANN102
per-file-ignores =
    tests/*:ANN201
    __init__.py:F401
# pyproject.toml — after, Ruff 0.6.x
[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "W", "ANN", "B", "UP", "TCH"]
ignore = ["ANN401"]              # ANN101/ANN102 were removed from Ruff; no self/cls rule

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["ANN201"]           # tests need not annotate returns
"__init__.py" = ["F401"]         # re-exports

Two migration notes. select replaces the checked set wholesale; extend-select adds to whatever Ruff already selects, so use it when you want to keep the defaults and bolt on TCH. And ANN101/ANN102 (missing annotation on self/cls) no longer exist in Ruff — those were always redundant, so just drop them rather than porting them into ignore.

Turn on autofix

Much of the value is that UP and TCH rules are auto-fixable. Ruff rewrites the code for you:

# Python 3.12, before ruff check --fix
from typing import List, Optional, Union

def load_ids(raw: Optional[List[Union[int, str]]]) -> List[int]:
    ...
# after ruff check --fix — UP006, UP007, UP045 applied
def load_ids(raw: list[int | str] | None) -> list[int]:
    ...
# Ruff 0.6.x — apply safe fixes, then report what remains
ruff check --fix .

Before you rely on UP007/UP045 rewrites, confirm your lowest supported interpreter accepts the new union operator — the matrix-testing guide shows how. The concept pages for the resulting forms are Union and Optional types and typing collections.

Runtime vs static analysis Ruff and flake8 are linters: they read your annotations as text and never verify them. ANN001 fires because an annotation is absent, not because it is wrong — Ruff will happily accept def f(x: int) -> str: return x. Only a type checker like mypy catches the [return-value] mismatch. And UP007's int | str rewrite is a runtime TypeError on Python < 3.10 unless the annotation is a string, which the linter does not check for you.

Wire it into CI and pre-commit

Replace the flake8 hook with Ruff’s, and add both the linter and formatter.

# .pre-commit-config.yaml — Ruff 0.6.x
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff             # lint; add --fix to auto-repair
        args: ["--fix"]
      - id: ruff-format      # replaces black
# .github/workflows/lint.yml — GitHub Actions
      - run: pip install ruff==0.6.9
      - run: ruff check --output-format=github .   # inline PR annotations
      - run: ruff format --check .

--output-format=github emits ::error workflow commands so each ANN201 or B006 lands as an inline annotation. See pre-commit hooks setup for the local-vs-CI split.

Common mistakes

  • Expecting Ruff to type-check. ANN rules only detect missing annotations; a wrong annotation still needs mypy’s [return-value] / [arg-type]. Ruff and a type checker are complementary, not substitutes.
  • Using select when you meant extend-select. select overwrites the default set, so listing only ["ANN"] silently disables E/F. Use extend-select to add on top of the defaults.
  • Porting ANN101/ANN102. They were removed from Ruff; keeping them in ignore is harmless clutter, and putting them in select errors out. Just delete them.
  • Leaving .flake8 behind. A stray .flake8 or setup.cfg [flake8] section confuses contributors and CI about which linter is authoritative. Delete it once [tool.ruff.lint] is in place.

FAQ

Does Ruff replace mypy? No. Ruff is a linter and formatter; it checks annotation presence and style (ANN, UP, TCH), not annotation correctness. Pair it with mypy or pyright, which do the actual type inference and catch [arg-type] / [return-value].

How do I keep flake8 and Ruff running side by side during the transition? You can, but disable overlapping rules on one side to avoid double reports — e.g. drop B and ANN from .flake8 once Ruff owns them. Most teams cut over in a single PR since the rule coverage maps cleanly.

Back to Ruff Linter Integration