跳转至

ruff:一个工具替掉 black + flake8 + isort + pyupgrade

各位还记不记得 2022 年那会儿,开一个 Python 新项目,光「保证代码风格统一」这件事就要装一堆东西:

  • black:自动格式化代码,管缩进、换行、引号
  • flake8:检查语法错误、未使用变量、命名不规范
  • isort:把 import 语句按字母顺序、按分组重新排列
  • pyupgrade:把老语法升级到新版本,比如 Dict[str, int] 改成 dict[str, int]
  • pydocstyle:检查 docstring 写得规不规范
  • bandit:扫一扫有没有安全漏洞

六个工具,六份配置文件,六套规则,每个工具自己装一遍,CI 上每个跑一遍,commit 之前每个调一遍。水哥当年配一个新项目的 lint 流水线,光研究这些工具怎么互相不打架就能花掉半天。

「不能合并一下吗?」当年很多童鞋都问过这个问题。

合不了。这些工具互相独立,技术栈也不一样:blackPython 写,flake8Python 写但内核是 pycodestyleisort 自己是个独立项目。要把它们捏到一起,意味着要重新实现一遍。

直到 2022 年底,Astral 公司——就是上一章那个写 uv 的 Astral——丢出来一个叫 ruff 的东西。这家公司有个特点:用 Rust 重写 Python 工具链,并且比原版快 10 到 100 倍

ruff 一上来就把 flake8isortpyupgradepydocstylebandit 这些工具的规则一口气全实现了,后来又补上了 format 子命令,把 black 的功能也吃掉了。一个二进制文件,一份配置,一套命令。

到 2024 年,ruff 已经是 Python 社区的事实标准,PandasFastAPIPydanticHugging Face TransformersApache Airflow 都换了过去。到 2026 年的今天,各位开新项目,几乎不会再有人推荐用 flake8 + black + isort 这套老组合了。

这一章就来讲讲 ruff 怎么用。学完之后,各位应该能:

  1. 在新项目里五分钟内配好 ruff
  2. 看懂别人 pyproject.toml 里那一坨 [tool.ruff] 配置
  3. ruff 接进 pre-commitCI、编辑器
  4. 把老项目从 black + flake8 + isort 平滑迁移过来

老办法到底烦在哪

先回忆一下老办法是怎么个用法。一个比较「正经」的项目,根目录下通常会出现这些配置:

my-project/
├── pyproject.toml         # 部分工具的配置
├── setup.cfg              # flake8 必须放这里(或单独的 .flake8)
├── .flake8                # flake8 配置(可选)
├── .isort.cfg             # isort 配置(可选)
├── .pre-commit-config.yaml
└── ...

每个工具有自己偏好的配置文件位置:

  • blackpyproject.toml[tool.black]
  • flake8 死活不支持 pyproject.toml,只能用 .flake8setup.cfg
  • isort 可以放 pyproject.toml.isort.cfgsetup.cfg,看心情
  • pyupgrade 没配置文件,全靠命令行参数

这就埋了第一个坑——配置散落各处,新人接手项目,光找配置文件就要找半天。

第二个坑是工具之间会打架。最经典的就是 blackflake8 冲突。black 默认行宽 88 字符,flake8 默认行宽 79 字符(PEP 8 推荐值)。两个不调一致,black 格式化完,flake8 就报 E501 line too long。怎么办?要么改 flake8,要么改 black,要么两边都加 # noqa 注释。

isortblack 也会打架。isort 默认按某种风格排 importblack 不一定接受,要给 isort 加上 --profile black 才能一致。

第三个坑是。这个慢不是「人感觉慢」,是真的慢——一个中型项目,跑 flake8 . 几秒到几十秒;跑 black . 几秒到十几秒;跑 isort . 也要几秒。CI 上串起来跑一遍,几十秒就没了。本地保存文件时想跑一遍 lint,慢到没法接进编辑器。

这就是 2022 年之前 Python 工程化的真实情况。各位老司机看到这里应该能共鸣。

ruff 是个啥

ruff 是 Astral 公司开源的 Python 代码检查 + 格式化工具。两个关键属性:

  1. 用 Rust 写的。Python 工具用 Python 写,速度天花板就在那里。换成 Rust,速度能上一两个数量级。
  2. 一个工具替掉一堆flake8blackisortpyupgradepydocstylebanditpylint 的部分规则、autoflake,全部内置。

看一下 ruff 官方公布的速度对比。在 CPython 项目(约 25 万行代码)上跑:

工具 耗时
flake8 12 秒左右
pylint 几分钟
ruff check 0.4 秒左右

各位读到这里可能怀疑:「真的有这么快吗,会不会是数据造假?」水哥亲自在自己电脑上跑过,结论是真的就是这么快。一个一千文件的项目,ruff check . 经常一秒之内出结果,肉眼看不到「跑」的过程。

ruff format 也类似。一个中等大小的项目,black 跑十几秒,ruff format 一秒不到。

这速度有什么用?两点重要:

  1. 保存文件时实时跑 lint 成为可能。编辑器里每次 Ctrl+Sruff 后台跑一遍,几乎感觉不到延迟。
  2. CI 时间短。从前 lint 阶段要 30 秒,现在 1 秒就过了。

「那它支持多少规则?」有童鞋想知道这个。截至 2026 年,ruff 已经实现了 800 多条规则,覆盖了 flake8 主线 + 几十个 flake8-* 插件 + isort + pyupgrade + pydocstyle + pylint 的一部分 + bandit 的一部分。换句话说,几乎所有主流 Python lint 工具的能力,ruff 都吃下来了。

安装

老规矩,推荐用 uv 装:

uv add --dev ruff

--dev 是啥意思?」上一章讲过,意思是「装到开发依赖组」,发布生产环境时不会带上。ruff 是开发工具,只在开发和 CI 时用,所以放 --dev 最合适。

如果项目还没用 uv,用 pip 装也行:

pip install ruff

或者用 pipx 装成全局命令:

pipx install ruff

装完之后,验证一下:

ruff --version

输出类似:

ruff 0.8.0

具体版本号会变,2026 年的 ruff 应该已经到 0.9 或者 1.0 之后了。

「为什么版本号都还没到 1.0?」有细心的童鞋会问。ruff 的作者一直在小步迭代,规则集还在持续扩充,所以版本号一直保持在 0.x。但稳定性其实早就生产级别了,社区里大量项目在用,没什么大问题。

第一次跑

进项目目录,跑一下:

ruff check .

这条命令会扫描当前目录下所有 .py 文件,按默认规则检查。各位第一次跑大概率会看到一堆告警:

foo.py:3:1: F401 [*] `os` imported but unused
foo.py:8:5: E731 Do not assign a `lambda` expression, use a `def`
bar.py:12:80: E501 Line too long (95 > 79)

格式很清晰:文件名:行:列: 规则号 描述。规则号前面如果带个 [*],说明这条规则可以自动修复

要让 ruff 自动修,加 --fix

ruff check --fix .

跑完之后,os imported but unused 这种问题,ruff 直接把那行 import os 删掉了。各位再跑一次 ruff check .,告警少了一大半。

格式化代码用另一个子命令:

ruff format .

这条命令的行为基本和 black . 一样——把整个项目的代码风格统一到一致的缩进、引号、换行规则。各位之前如果用过 black,这一步零学习成本。

把这两条命令加在一起,就是日常 commit 之前的标准流程:

ruff check --fix . && ruff format .

先 lint 再 format,齐活。

配置:pyproject.toml

ruff 的配置全部塞进 pyproject.toml,没有别的文件。配置长这样:

[tool.ruff]
# 行宽,跟 black 默认一致
line-length = 88

# 目标 Python 版本,影响某些规则的判断
target-version = "py312"

# 排除哪些目录
exclude = [
    ".git",
    ".venv",
    "build",
    "dist",
    "__pycache__",
    "migrations",
]

[tool.ruff.lint]
# 启用哪些规则集
select = ["E", "F", "I", "UP", "B", "SIM"]

# 忽略哪些具体规则
ignore = ["E501"]

[tool.ruff.format]
# 引号风格:双引号优先(black 风格)
quote-style = "double"
# 缩进风格:空格
indent-style = "space"

各位看着这一坨可能有点懵,下面挨个拆开讲。

line-length

行宽。默认 88,跟 black 一致。喜欢 100 也行:

line-length = 100

target-version

目标 Python 版本。这个值影响一些规则的判断。比如 target-version = "py312"ruff 知道 dict[str, int] 这种语法你能用,会建议把老代码里的 Dict[str, int] 改过来。如果你写 target-version = "py38"ruff 就不会做这种建议(因为 3.8 不支持)。

合法值:py38py39py310py311py312py313py314

exclude

不扫描哪些目录。.git.venvbuilddist 这些 ruff 默认就会排除,但是 migrations(Django 项目自动生成的迁移文件)这种,需要自己加。

select

最重要的字段——启用哪些规则集ruff 用「字母前缀」给规则分组:

  • Epycodestyle 错误(PEP 8 风格规则)
  • Wpycodestyle 警告
  • Fpyflakes(未使用变量、未定义引用等)
  • Iisort(import 排序)
  • UPpyupgrade(语法升级建议)
  • Bflake8-bugbear(常见 bug 模式)
  • SIMflake8-simplify(简化代码建议)
  • Npep8-naming(命名规范)
  • Dpydocstyle(docstring 规范)
  • Sflake8-bandit(安全检查)
  • ANNflake8-annotations(类型注解)
  • C4flake8-comprehensions(推导式优化)
  • RETflake8-return(return 语句规范)
  • ARGflake8-unused-arguments(未使用参数)

每个前缀下面还有具体规则,比如 E501 是「行太长」、F401 是「import 但没用」。

新项目第一次配,推荐这一套:

select = ["E", "F", "I", "UP", "B", "SIM"]

E + F 是基本款(PEP 8 + pyflakes),I 管 import 排序,UP 把老语法升级到新版本,B 抓常见 bug,SIM 给一些简化建议。这六组覆盖了 80% 的需求,又不会噪声太大。

如果项目对代码质量要求高,再加上 N(命名规范)、C4(推导式)、RET(return)、ARG(未用参数)。

如果还要加 docstring 检查(D),各位要做好心理准备——pydocstyle 的规则很严格,老项目一开就是几百条告警。建议从 select = ["D"] + ignore = ["D100", "D101", "D102", ...] 一条条试,别一上来就全开。

ignore

忽略某些具体规则。比如 E501(行太长),有些项目觉得 88 字符不够用,就加进 ignore

ignore = ["E501"]

或者忽略整个组:

ignore = ["D"]

per-file-ignores

按文件忽略规则。__init__.py 经常有未使用的 import(其实是用来重导出的),不想被告警,可以这样:

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/*" = ["S101", "ANN"]

tests/* 里允许用 assertS101),不要求每个测试函数都写类型注解(ANN)。

isort 子配置

I 规则集(isort 替代)有自己的子配置,放 [tool.ruff.lint.isort]

[tool.ruff.lint.isort]
# 把这些当成「第一方」包,跟其他第三方分开排
known-first-party = ["my_project"]

# import 块的分组顺序
section-order = [
    "future",
    "standard-library",
    "third-party",
    "first-party",
    "local-folder",
]

known-first-party 这个配置很常用——告诉 ruffmy_project 是我自己的包」,ruff 就会把它的 import 放到独立的一组,不跟第三方包混在一起。

完整配置示例

放一份生产可用的 pyproject.toml 配置,各位可以直接抄回去改:

[tool.ruff]
line-length = 88
target-version = "py312"
exclude = [
    ".git",
    ".venv",
    "build",
    "dist",
    "__pycache__",
    "migrations",
    "*.ipynb",
]

[tool.ruff.lint]
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort
    "UP",  # pyupgrade
    "B",   # flake8-bugbear
    "SIM", # flake8-simplify
    "C4",  # flake8-comprehensions
    "N",   # pep8-naming
    "RET", # flake8-return
]
ignore = [
    "E501",  # 行太长,交给 formatter 处理
    "B008",  # 函数默认参数里调用函数,FastAPI 的 Depends 用法
]

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

[tool.ruff.lint.isort]
known-first-party = ["my_project"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"

抄这份过去,改一下 target-versionknown-first-party,基本就能用。

常用规则集一览

把上面提到的规则集整理成一张对照表,方便各位查:

前缀 来源工具 管什么
E pycodestyle PEP 8 风格错误(缩进、空格)
W pycodestyle PEP 8 风格警告
F pyflakes 未使用变量、未定义引用、import 错
I isort import 排序与分组
UP pyupgrade 老语法升级到新版本
B flake8-bugbear 常见 bug 模式
SIM flake8-simplify 代码简化建议
N pep8-naming 命名规范(驼峰、下划线)
D pydocstyle docstring 规范
S flake8-bandit 安全漏洞检查
ANN flake8-annotations 类型注解检查
C4 flake8-comprehensions 推导式优化
RET flake8-return return 语句风格
ARG flake8-unused-arguments 未使用参数
PT flake8-pytest-style pytest 风格
PL pylint pylint 部分规则
RUF ruff ruff 自家原创规则

完整清单官方文档里有,叫「Rules」页面,规则号、说明、是否能自动修都列得清清楚楚。各位用到哪条规则不懂,搜一下规则号就行。

自动修复实战

ruff check --fix . 能修哪些东西?挑几个常见的看看。

F401:未使用的 import

import os
import sys

print(sys.version)

ruff check --fix 之后:

import sys

print(sys.version)

os 没用上,直接删掉。

E711:和 None 比较应该用 is

if x == None:
    print("x 是 None")

修复后:

if x is None:
    print("x 是 None")

为什么要改?因为 == 会触发 __eq__ 方法,有些类的 __eq__ 实现得稀奇古怪(比如 numpy 数组),跟 None 比较会出意想不到的结果。is None 是身份比较,永远只看「是不是同一个对象」,绝对安全。

C408:不必要的 list/dict/tuple 调用

empty = list()
d = dict()
t = tuple()

修复后:

empty = []
d = {}
t = ()

list()[] 等价,但是 list() 要去全局名字空间里查 list 这个名字、再调用,慢一点。直接写字面量更快也更短。

UP006:用新版类型注解

from typing import List, Dict

def foo(x: List[int]) -> Dict[str, int]:
    pass

修复后(target-version = "py39" 以上):

def foo(x: list[int]) -> dict[str, int]:
    pass

Python 3.9+ 已经支持直接用 list[int] 写类型注解,不用再从 typing 里 import 了。ruff 自动帮各位升级。

UP008:用 super() 不要带参数

class Foo(Bar):
    def __init__(self):
        super(Foo, self).__init__()

修复后:

class Foo(Bar):
    def __init__(self):
        super().__init__()

Python 3 里 super() 不传参数就够了,老 Python 2 风格的 super(Foo, self) 是历史包袱。

SIM108:能用三元表达式不要用 if-else

if x > 0:
    y = "positive"
else:
    y = "non-positive"

ruff 会建议(这条不是自动修,因为牵涉可读性,需要人判断):

y = "positive" if x > 0 else "non-positive"

I001:import 排序

import sys
from my_project import utils
import os
import requests

修复后:

import os
import sys

import requests

from my_project import utils

ruff 把 import 按「标准库 → 第三方 → 第一方」分成三组,每组之间空一行,组内按字母序排。这等同于跑了一遍 isort

不安全修复

有些修复 ruff 不会默认做,因为可能改变行为。比如把 dict() 改成 {}——99% 情况下等价,但是如果你的代码里 dict 这个名字被覆盖了,行为就变了。这种修复叫「unsafe fix」,要加 --unsafe-fixes 才会做:

ruff check --fix --unsafe-fixes .

各位如果心里没底,先 ruff check --fix . 跑安全的,再用 --unsafe-fixes 单独跑一遍并 review 改动。

ruff format:black 替代品

ruff format . 干的事跟 black . 几乎一样:

  • 行宽默认 88
  • 字符串默认双引号
  • 缩进 4 空格
  • 函数参数过长自动换行
  • 字典、列表、set 字面量按一致风格排版

格式化前:

def foo(a,b,c, d ):
    return  {  'name':a,'age':b,'list':[1,2,3]  }

ruff format . 之后:

def foo(a, b, c, d):
    return {"name": a, "age": b, "list": [1, 2, 3]}

black 输出几乎完全一致。Astral 在文档里明确写过「ruff formatblack 99.9% 兼容」,各位从 black 迁过来不用担心代码风格突变。

少量差异确实存在,主要在边缘情况(极长行、特殊魔法注释等),跑一遍 ruff format,git diff 一看就知道。

ruff format 也支持 --check 模式,只检查不改:

ruff format --check .

如果有文件需要格式化,命令返回非零 exit code,CI 上可以拿来卡住「忘了 format 就提交」的 PR。

集成 pre-commit

pre-commit 是一个 git hook 框架,让各位在 git commit 时自动跑一些检查。装一下:

uv add --dev pre-commit
pre-commit install

然后在项目根目录建 .pre-commit-config.yaml

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

这一段配置干了两件事:

  1. ruff hook:跑 ruff check --fix,自动修能修的
  2. ruff-format hook:跑 ruff format

每次 git commitpre-commit 会自动跑这两个 hook,跑通才让 commit。

rev: v0.8.0 怎么填?」pre-commit 上有命令自动填最新版:

pre-commit autoupdate

跑一下,.pre-commit-config.yaml 里的版本号就更新到最新了。建议各位每隔几个月跑一次。

第一次装好 pre-commit,可以手动跑一遍:

pre-commit run --all-files

把项目里所有文件都过一遍,把历史欠债一次性清干净。

「为什么不直接用 ruff 自己的 hook?」有童鞋会问。ruff 没有自己的 git hook 机制,要靠 pre-commit 这种通用框架。pre-commit 是 Python 圈里通用方案,除了 ruff 还能挂别的 hook(比如 mypyprettier),统一管理。

集成 GitHub Actions CI

ruff 在 CI 上的用法非常简单。新建 .github/workflows/lint.yml

name: Lint

on:
  push:
    branches: [main]
  pull_request:

jobs:
  ruff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install uv
        uses: astral-sh/setup-uv@v3

      - name: Install ruff
        run: uv tool install ruff

      - name: Run ruff check
        run: ruff check --output-format=github .

      - name: Run ruff format check
        run: ruff format --check .

--output-format=github 这个选项很重要——它把告警输出成 GitHub Actions 能识别的格式,PR 页面上会直接在出问题的代码行上显示一个小标记,鼠标悬停就能看到具体规则和说明。比纯文本日志好用得多。

也有官方做的 action,更省事:

- name: Run ruff
  uses: astral-sh/ruff-action@v3

这个 action 自动装 ruff 并跑 ruff check,一行搞定。各位有兴趣可以去 GitHub 搜 astral-sh/ruff-action 看文档。

CI 时间方面,ruff 这部分通常 5 秒以内跑完(包括环境准备),比从前 flake8 + black + isort 那一套快太多。

「能在 PR 上自动修复并提交回来吗?」可以,但是要小心处理权限和分叉 PR。一般做法是:CI 上只检查不修,由开发者自己在本地修完再推。要做自动修复 PR,得用专门的 bot 账号 + GitHub App,超出本章范围。

集成编辑器

代码风格工具最后一公里——编辑器集成。装好之后,每次保存文件,编辑器自动跑 ruff,问题立刻显示在出错的行上。这种体验和 CI、pre-commit 是不可替代的:CI 是兜底,编辑器是日常。

VS Code

装这个插件:Ruff (charliermarsh.ruff)

装完之后,settings.json 里加几行:

{
    "[python]": {
        "editor.defaultFormatter": "charliermarsh.ruff",
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
            "source.fixAll.ruff": "explicit",
            "source.organizeImports.ruff": "explicit"
        }
    }
}

效果:

  • 保存文件时自动跑 ruff format
  • 自动应用所有能自动修的 lint 修复
  • 自动整理 import

写代码的时候,每次 Ctrl+Sruff 在背后跑一遍,代码自动整齐。

PyCharm

PyCharm 有官方支持的 Ruff 插件,去 Settings → Plugins 搜「Ruff」装上。

装完之后:

  • Settings → Tools → Ruff:勾上「Use ruff format」、「Run ruff on save」
  • Settings → Editor → Code Style → Python:行宽改成 88,跟 ruff 一致

PyCharm 的好处是检查在编辑时实时显示,不用等保存。问题代码会有黄色波浪线,鼠标悬停看具体规则。

Neovim / Vim

通过 nvim-lspconfig + mason.nvimruff 的 LSP server:

require("lspconfig").ruff.setup({})

或者直接用 null-ls / none-lsruff 集成。具体用哪种取决于各位的 Neovim 配置框架。

从 black + flake8 + isort 迁移

老项目要换过来,怎么办?步骤:

第一步:装 ruff

uv add --dev ruff

第二步:写 ruff 配置

参考前面的「完整配置示例」,写一份 [tool.ruff]pyproject.tomlline-lengthtarget-version 等关键参数对齐原来 black 的配置。

第三步:跑一遍看看差异

ruff check .
ruff format --check .

ruff check . 看 lint 告警,ruff format --check . 看格式化差异。

ruff formatblack 99.9% 兼容,但还是会有少量文件被改。各位先 ruff format . 跑一遍,git diff 看一下,确认改动可接受再提交。

第四步:删掉老工具

uv remove --dev black flake8 isort pyupgrade

如果项目还在用 pip + requirements-dev.txt,从那里删。

第五步:删配置

.flake8 删掉,setup.cfg 里的 [flake8] 段删掉,pyproject.toml 里的 [tool.black][tool.isort] 段删掉。

第六步:更新 pre-commit 和 CI

.pre-commit-config.yaml 里把 blackflake8isort 的 hook 删掉,换成 ruff 的。CI 同理。

第七步:通知团队

发一条消息给团队:「项目从 X 月 X 日起切换到 ruff,请大家拉最新代码、跑 pre-commit install 重装 hook」。一般这个步骤不会有什么问题,因为 ruff formatblack 兼容,原来 black 格式化过的代码 ruff 不会再大改。

ruff vs black + flake8 + isort 对照表

最后放一张对照表,方便各位心里有个底:

维度 老组合 ruff
工具数量 至少 3 个(black + flake8 + isort),加 pyupgrade、pydocstyle 更多 1 个
配置位置 散落在 pyproject.toml.flake8setup.cfg 全部在 pyproject.toml
速度 几秒到几十秒 通常 1 秒以内
规则数量 各家加起来约 500 条 800+ 条
自动修复 部分支持(black、isort、pyupgrade 自动;flake8 不修) 大部分规则支持
实现语言 Python Rust
二进制依赖 安装 Python 包 单二进制(也能 pip 装)
编辑器实时 lint 慢,体验差 快,体验丝滑
pyproject.toml 支持 部分(flake8 不支持) 完全支持

ruff 自家规则:RUF 系列

ruff 除了搬运别家规则,也自己原创了一些规则,前缀是 RUF。挑几个常见的看看。

RUF001/RUF002/RUF003:模糊字符

代码里出现「看着是英文字母但其实是希腊字母、全角字符」这种字符,会触发 RUF001。比如:

result = data["nаme"]  # 这里的 а 是西里尔字母 а,不是英文 a

肉眼几乎看不出来,但是程序跑起来会找不到键报 KeyErrorruff 能识别出这种字符并告警,避免各位调试到怀疑人生。

RUF005:用解包代替 list 拼接

new_list = [1, 2, 3] + list(other)

ruff 建议改成:

new_list = [1, 2, 3, *other]

解包语法更快、更短,还能直接拼任何可迭代对象,不用先转成 list。

RUF013:隐式 Optional 类型

def greet(name: str = None):
    print(f"Hello, {name}")

这个写法很常见,但其实有问题——name 默认是 None,但类型注解写的是 str,前后不一致。ruff 会建议改成:

def greet(name: str | None = None):
    print(f"Hello, {name}")

老 Python 写法是 Optional[str],新版 Python(3.10+)直接用 str | None 更清爽。

RUF100:未使用的 noqa

x = 1  # noqa: F401

如果这一行其实没有任何 lint 告警,那个 # noqa 就是垃圾。ruff 会专门把这种「过期 noqa」标出来,提醒各位删掉。

这条规则非常实用——老项目迁移到 ruff 之后,原来给 flake8 加的 # noqa 大部分会过时,靠 RUF100 一扫一个准。

一个常见坑:和 mypy 的关系

很多童鞋会问:「ruff 能替代 mypy 吗?」

不能。

ruff 是 lint + formatter,做的是「代码风格、常见错误模式」检查。mypy类型检查器,做的是「这个变量传给那个函数,类型对不对」这种全局类型推导。两者是不同维度的工具,互补使用。

正确的姿势:

  • ruff 管风格、import、明显 bug、语法升级
  • mypy 或者 pyright 管类型

CI 上两个都跑:

- run: ruff check .
- run: ruff format --check .
- run: mypy .

ruff 倒是有几条规则(前缀 ANN)会做粗浅的类型注解检查,比如「这个函数没有写 return 类型」。但是它不会真正去推导类型一致性,复杂的类型问题还是要靠 mypy

一个进阶玩法:分目录用不同规则

大项目经常会有「核心代码严格管,脚本目录放松点」的需求。ruff 0.5 之后支持子目录覆盖配置,写法是 [tool.ruff.lint.per-file-ignores] 加 glob 模式。前面提过简单用法,下面看更复杂的:

[tool.ruff.lint.per-file-ignores]
# 测试文件允许 assert,允许长一点
"tests/**/*.py" = ["S101", "ANN", "D"]

# 一次性脚本,规则放最松
"scripts/*.py" = ["ALL"]

# 文档示例代码,允许 print
"docs/examples/*.py" = ["T20"]

# 迁移文件不动它
"**/migrations/*.py" = ["E", "F", "I", "UP", "B"]

"ALL" 是个特殊值,意思是「所有规则都忽略」。各位看到 scripts/*.py 那条,意思是脚本目录里 ruff 啥都不管,怎么写都行。

嵌套配置

如果你的项目是 monorepo,多个子项目共存,每个子项目想要自己的 ruff 配置,可以在子目录里放一个独立的 pyproject.tomlruff.toml

mono/
├── pyproject.toml          # 根配置
├── service-a/
│   ├── pyproject.toml      # service-a 自己的配置(可选)
│   └── ...
└── service-b/
    ├── ruff.toml           # service-b 自己的配置
    └── ...

ruff 在扫描每个文件时,会向上查找最近的配置文件。各子项目互不干扰。

一个进阶玩法:noqa 注释

有些时候你确实想让 ruff 闭嘴。比如某行代码里 eval() 用得有充分理由,但 S307flake8-bandit)会告警。这时候可以加 # noqa 注释:

result = eval(user_input)  # noqa: S307

# noqa: S307 表示「这行代码忽略 S307 这条规则」。多条规则用逗号隔开:

result = eval(user_input)  # noqa: S307, S102

裸的 # noqa(不指定规则号)也行,但是不推荐——会把这行所有规则都忽略,相当于关大灯。指定规则号最精准。

各位写 # noqa 之前最好先想想:是这个规则不合理,还是代码本身就该改?大多数时候是后者。

几个易踩的坑

最后讲几个迁移到 ruff 之后老司机也会踩的坑,各位提前知道少走弯路。

坑 1:select 写错前缀,规则不生效

select = ["E1"] 不会启用 pycodestyle E1xx 这一组,只会启用 E1 这一条(如果有的话)。要启用 E1xx 整组,要写 select = ["E1"] 或者更宽的 select = ["E"]

具体规则:

  • "E":启用所有 E 开头的规则(E1xx、E2xx、E5xx...)
  • "E1":启用所有 E1xx 开头的规则
  • "E101":只启用 E101 这一条

各位记不住的话,先用粗粒度 ["E", "F", "I"] 这种,能跑就行。

坑 2:select 和 ignore 同时写

select = ["E"]
ignore = ["E501"]

这样配的意思是「启用 E 全组,但 E501 这条不要」。这是合法的,ignore 优先级高。各位经常会看到这种「开一组+排除一两条」的写法,是标准用法。

坑 3:ruff format 不读 [tool.ruff.lint] 的配置

[tool.ruff.lint] 管 lint,[tool.ruff.format] 管 format,两者完全独立。最常见的踩坑是:在 [tool.ruff.lint] 里设了 quote-style = "double",结果 ruff format 还是按默认风格跑,因为 quote-style 应该写在 [tool.ruff.format] 里。

# 错误位置
[tool.ruff.lint]
quote-style = "double"  # 这里写了没用

# 正确位置
[tool.ruff.format]
quote-style = "double"

坑 4:preview 规则

ruff 还有一批正在开发的「preview」规则,默认不启用。要打开:

[tool.ruff.lint]
preview = true

或者命令行 ruff check --preview。preview 规则的好处是抢先用上新东西,坏处是规则号、行为可能在版本之间变。生产项目稳着来,不建议默认开 preview

坑 5:忘了升级 pre-commit hook 版本

各位 .pre-commit-config.yaml 里的 rev: v0.8.0 是写死的,ruff 升了新版本不会自动同步。半年之后老 ruff 跑不出新规则,团队成员之间 ruff 版本不一致也会出问题。

建议:每个月或每个季度跑一次 pre-commit autoupdate,把 hook 版本统一升一下。CI 也跟着升。

小结

各位走完这一章,应该已经能:

  1. ruffuv add --dev ruff
  2. ruffruff check --fix .ruff format .
  3. ruffpyproject.toml 里写 [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM"]
  4. pre-commit:写 .pre-commit-config.yaml,加 ruff-pre-commit 仓库
  5. CI:GitHub Actions 用 astral-sh/ruff-action@v3
  6. 接编辑器:VS Code 装 charliermarsh.ruff 插件,配置保存时自动 format

这套工作流配好之后,新项目从零到「能自动检查、能自动修复、能 CI 兜底、能编辑器实时反馈」只需要十分钟。代码风格的事,交给 ruff 就行,各位省下来的精力可以用来写真正的业务代码。

下一章我们看 pytest——怎么写好 Python 项目的测试。uv 管依赖、ruff 管风格、pytest 管测试,这三件套配齐,2026 年的 Python 工程化就齐活了。