ruff:一个工具替掉 black + flake8 + isort + pyupgrade¶
各位还记不记得 2022 年那会儿,开一个 Python 新项目,光「保证代码风格统一」这件事就要装一堆东西:
black:自动格式化代码,管缩进、换行、引号flake8:检查语法错误、未使用变量、命名不规范isort:把import语句按字母顺序、按分组重新排列pyupgrade:把老语法升级到新版本,比如Dict[str, int]改成dict[str, int]pydocstyle:检查 docstring 写得规不规范bandit:扫一扫有没有安全漏洞
六个工具,六份配置文件,六套规则,每个工具自己装一遍,CI 上每个跑一遍,commit 之前每个调一遍。水哥当年配一个新项目的 lint 流水线,光研究这些工具怎么互相不打架就能花掉半天。
「不能合并一下吗?」当年很多童鞋都问过这个问题。
合不了。这些工具互相独立,技术栈也不一样:black 用 Python 写,flake8 用 Python 写但内核是 pycodestyle,isort 自己是个独立项目。要把它们捏到一起,意味着要重新实现一遍。
直到 2022 年底,Astral 公司——就是上一章那个写 uv 的 Astral——丢出来一个叫 ruff 的东西。这家公司有个特点:用 Rust 重写 Python 工具链,并且比原版快 10 到 100 倍。
ruff 一上来就把 flake8、isort、pyupgrade、pydocstyle、bandit 这些工具的规则一口气全实现了,后来又补上了 format 子命令,把 black 的功能也吃掉了。一个二进制文件,一份配置,一套命令。
到 2024 年,ruff 已经是 Python 社区的事实标准,Pandas、FastAPI、Pydantic、Hugging Face Transformers、Apache Airflow 都换了过去。到 2026 年的今天,各位开新项目,几乎不会再有人推荐用 flake8 + black + isort 这套老组合了。
这一章就来讲讲 ruff 怎么用。学完之后,各位应该能:
- 在新项目里五分钟内配好
ruff - 看懂别人
pyproject.toml里那一坨[tool.ruff]配置 - 把
ruff接进pre-commit、CI、编辑器 - 把老项目从
black + flake8 + isort平滑迁移过来
老办法到底烦在哪¶
先回忆一下老办法是怎么个用法。一个比较「正经」的项目,根目录下通常会出现这些配置:
my-project/
├── pyproject.toml # 部分工具的配置
├── setup.cfg # flake8 必须放这里(或单独的 .flake8)
├── .flake8 # flake8 配置(可选)
├── .isort.cfg # isort 配置(可选)
├── .pre-commit-config.yaml
└── ...
每个工具有自己偏好的配置文件位置:
black看pyproject.toml的[tool.black]flake8死活不支持pyproject.toml,只能用.flake8或setup.cfgisort可以放pyproject.toml、.isort.cfg、setup.cfg,看心情pyupgrade没配置文件,全靠命令行参数
这就埋了第一个坑——配置散落各处,新人接手项目,光找配置文件就要找半天。
第二个坑是工具之间会打架。最经典的就是 black 和 flake8 冲突。black 默认行宽 88 字符,flake8 默认行宽 79 字符(PEP 8 推荐值)。两个不调一致,black 格式化完,flake8 就报 E501 line too long。怎么办?要么改 flake8,要么改 black,要么两边都加 # noqa 注释。
isort 和 black 也会打架。isort 默认按某种风格排 import,black 不一定接受,要给 isort 加上 --profile black 才能一致。
第三个坑是慢。这个慢不是「人感觉慢」,是真的慢——一个中型项目,跑 flake8 . 几秒到几十秒;跑 black . 几秒到十几秒;跑 isort . 也要几秒。CI 上串起来跑一遍,几十秒就没了。本地保存文件时想跑一遍 lint,慢到没法接进编辑器。
这就是 2022 年之前 Python 工程化的真实情况。各位老司机看到这里应该能共鸣。
ruff 是个啥¶
ruff 是 Astral 公司开源的 Python 代码检查 + 格式化工具。两个关键属性:
- 用 Rust 写的。Python 工具用 Python 写,速度天花板就在那里。换成 Rust,速度能上一两个数量级。
- 一个工具替掉一堆。
flake8、black、isort、pyupgrade、pydocstyle、bandit、pylint的部分规则、autoflake,全部内置。
看一下 ruff 官方公布的速度对比。在 CPython 项目(约 25 万行代码)上跑:
| 工具 | 耗时 |
|---|---|
flake8 |
12 秒左右 |
pylint |
几分钟 |
ruff check |
0.4 秒左右 |
各位读到这里可能怀疑:「真的有这么快吗,会不会是数据造假?」水哥亲自在自己电脑上跑过,结论是真的就是这么快。一个一千文件的项目,ruff check . 经常一秒之内出结果,肉眼看不到「跑」的过程。
ruff format 也类似。一个中等大小的项目,black 跑十几秒,ruff format 一秒不到。
这速度有什么用?两点重要:
- 保存文件时实时跑 lint 成为可能。编辑器里每次
Ctrl+S,ruff后台跑一遍,几乎感觉不到延迟。 - CI 时间短。从前 lint 阶段要 30 秒,现在 1 秒就过了。
「那它支持多少规则?」有童鞋想知道这个。截至 2026 年,ruff 已经实现了 800 多条规则,覆盖了 flake8 主线 + 几十个 flake8-* 插件 + isort + pyupgrade + pydocstyle + pylint 的一部分 + bandit 的一部分。换句话说,几乎所有主流 Python lint 工具的能力,ruff 都吃下来了。
安装¶
老规矩,推荐用 uv 装:
「--dev 是啥意思?」上一章讲过,意思是「装到开发依赖组」,发布生产环境时不会带上。ruff 是开发工具,只在开发和 CI 时用,所以放 --dev 最合适。
如果项目还没用 uv,用 pip 装也行:
或者用 pipx 装成全局命令:
装完之后,验证一下:
输出类似:
具体版本号会变,2026 年的 ruff 应该已经到 0.9 或者 1.0 之后了。
「为什么版本号都还没到 1.0?」有细心的童鞋会问。ruff 的作者一直在小步迭代,规则集还在持续扩充,所以版本号一直保持在 0.x。但稳定性其实早就生产级别了,社区里大量项目在用,没什么大问题。
第一次跑¶
进项目目录,跑一下:
这条命令会扫描当前目录下所有 .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:
跑完之后,os imported but unused 这种问题,ruff 直接把那行 import os 删掉了。各位再跑一次 ruff check .,告警少了一大半。
格式化代码用另一个子命令:
这条命令的行为基本和 black . 一样——把整个项目的代码风格统一到一致的缩进、引号、换行规则。各位之前如果用过 black,这一步零学习成本。
把这两条命令加在一起,就是日常 commit 之前的标准流程:
先 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 也行:
target-version¶
目标 Python 版本。这个值影响一些规则的判断。比如 target-version = "py312",ruff 知道 dict[str, int] 这种语法你能用,会建议把老代码里的 Dict[str, int] 改过来。如果你写 target-version = "py38",ruff 就不会做这种建议(因为 3.8 不支持)。
合法值:py38、py39、py310、py311、py312、py313、py314。
exclude¶
不扫描哪些目录。.git、.venv、build、dist 这些 ruff 默认就会排除,但是 migrations(Django 项目自动生成的迁移文件)这种,需要自己加。
select¶
最重要的字段——启用哪些规则集。ruff 用「字母前缀」给规则分组:
E:pycodestyle错误(PEP 8 风格规则)W:pycodestyle警告F:pyflakes(未使用变量、未定义引用等)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(未使用参数)
每个前缀下面还有具体规则,比如 E501 是「行太长」、F401 是「import 但没用」。
新项目第一次配,推荐这一套:
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:
或者忽略整个组:
per-file-ignores¶
按文件忽略规则。__init__.py 经常有未使用的 import(其实是用来重导出的),不想被告警,可以这样:
tests/* 里允许用 assert(S101),不要求每个测试函数都写类型注解(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 这个配置很常用——告诉 ruff「my_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-version 和 known-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¶
ruff check --fix 之后:
os 没用上,直接删掉。
E711:和 None 比较应该用 is¶
修复后:
为什么要改?因为 == 会触发 __eq__ 方法,有些类的 __eq__ 实现得稀奇古怪(比如 numpy 数组),跟 None 比较会出意想不到的结果。is None 是身份比较,永远只看「是不是同一个对象」,绝对安全。
C408:不必要的 list/dict/tuple 调用¶
修复后:
list() 和 [] 等价,但是 list() 要去全局名字空间里查 list 这个名字、再调用,慢一点。直接写字面量更快也更短。
UP006:用新版类型注解¶
修复后(target-version = "py39" 以上):
Python 3.9+ 已经支持直接用 list[int] 写类型注解,不用再从 typing 里 import 了。ruff 自动帮各位升级。
UP008:用 super() 不要带参数¶
修复后:
Python 3 里 super() 不传参数就够了,老 Python 2 风格的 super(Foo, self) 是历史包袱。
SIM108:能用三元表达式不要用 if-else¶
ruff 会建议(这条不是自动修,因为牵涉可读性,需要人判断):
I001:import 排序¶
修复后:
ruff 把 import 按「标准库 → 第三方 → 第一方」分成三组,每组之间空一行,组内按字母序排。这等同于跑了一遍 isort。
不安全修复¶
有些修复 ruff 不会默认做,因为可能改变行为。比如把 dict() 改成 {}——99% 情况下等价,但是如果你的代码里 dict 这个名字被覆盖了,行为就变了。这种修复叫「unsafe fix」,要加 --unsafe-fixes 才会做:
各位如果心里没底,先 ruff check --fix . 跑安全的,再用 --unsafe-fixes 单独跑一遍并 review 改动。
ruff format:black 替代品¶
ruff format . 干的事跟 black . 几乎一样:
- 行宽默认 88
- 字符串默认双引号
- 缩进 4 空格
- 函数参数过长自动换行
- 字典、列表、
set字面量按一致风格排版
格式化前:
ruff format . 之后:
跟 black 输出几乎完全一致。Astral 在文档里明确写过「ruff format 与 black 99.9% 兼容」,各位从 black 迁过来不用担心代码风格突变。
少量差异确实存在,主要在边缘情况(极长行、特殊魔法注释等),跑一遍 ruff format,git diff 一看就知道。
ruff format 也支持 --check 模式,只检查不改:
如果有文件需要格式化,命令返回非零 exit code,CI 上可以拿来卡住「忘了 format 就提交」的 PR。
集成 pre-commit¶
pre-commit 是一个 git hook 框架,让各位在 git commit 时自动跑一些检查。装一下:
然后在项目根目录建 .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
这一段配置干了两件事:
ruffhook:跑ruff check --fix,自动修能修的ruff-formathook:跑ruff format
每次 git commit,pre-commit 会自动跑这两个 hook,跑通才让 commit。
「rev: v0.8.0 怎么填?」pre-commit 上有命令自动填最新版:
跑一下,.pre-commit-config.yaml 里的版本号就更新到最新了。建议各位每隔几个月跑一次。
第一次装好 pre-commit,可以手动跑一遍:
把项目里所有文件都过一遍,把历史欠债一次性清干净。
「为什么不直接用 ruff 自己的 hook?」有童鞋会问。ruff 没有自己的 git hook 机制,要靠 pre-commit 这种通用框架。pre-commit 是 Python 圈里通用方案,除了 ruff 还能挂别的 hook(比如 mypy、prettier),统一管理。
集成 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,更省事:
这个 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+S,ruff 在背后跑一遍,代码自动整齐。
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.nvim 装 ruff 的 LSP server:
或者直接用 null-ls / none-ls 的 ruff 集成。具体用哪种取决于各位的 Neovim 配置框架。
从 black + flake8 + isort 迁移¶
老项目要换过来,怎么办?步骤:
第一步:装 ruff¶
第二步:写 ruff 配置¶
参考前面的「完整配置示例」,写一份 [tool.ruff] 进 pyproject.toml。line-length、target-version 等关键参数对齐原来 black 的配置。
第三步:跑一遍看看差异¶
ruff check . 看 lint 告警,ruff format --check . 看格式化差异。
ruff format 跟 black 99.9% 兼容,但还是会有少量文件被改。各位先 ruff format . 跑一遍,git diff 看一下,确认改动可接受再提交。
第四步:删掉老工具¶
如果项目还在用 pip + requirements-dev.txt,从那里删。
第五步:删配置¶
.flake8 删掉,setup.cfg 里的 [flake8] 段删掉,pyproject.toml 里的 [tool.black]、[tool.isort] 段删掉。
第六步:更新 pre-commit 和 CI¶
.pre-commit-config.yaml 里把 black、flake8、isort 的 hook 删掉,换成 ruff 的。CI 同理。
第七步:通知团队¶
发一条消息给团队:「项目从 X 月 X 日起切换到 ruff,请大家拉最新代码、跑 pre-commit install 重装 hook」。一般这个步骤不会有什么问题,因为 ruff format 和 black 兼容,原来 black 格式化过的代码 ruff 不会再大改。
ruff vs black + flake8 + isort 对照表¶
最后放一张对照表,方便各位心里有个底:
| 维度 | 老组合 | ruff |
|---|---|---|
| 工具数量 | 至少 3 个(black + flake8 + isort),加 pyupgrade、pydocstyle 更多 | 1 个 |
| 配置位置 | 散落在 pyproject.toml、.flake8、setup.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。比如:
肉眼几乎看不出来,但是程序跑起来会找不到键报 KeyError。ruff 能识别出这种字符并告警,避免各位调试到怀疑人生。
RUF005:用解包代替 list 拼接¶
ruff 建议改成:
解包语法更快、更短,还能直接拼任何可迭代对象,不用先转成 list。
RUF013:隐式 Optional 类型¶
这个写法很常见,但其实有问题——name 默认是 None,但类型注解写的是 str,前后不一致。ruff 会建议改成:
老 Python 写法是 Optional[str],新版 Python(3.10+)直接用 str | None 更清爽。
RUF100:未使用的 noqa¶
如果这一行其实没有任何 lint 告警,那个 # noqa 就是垃圾。ruff 会专门把这种「过期 noqa」标出来,提醒各位删掉。
这条规则非常实用——老项目迁移到 ruff 之后,原来给 flake8 加的 # noqa 大部分会过时,靠 RUF100 一扫一个准。
一个常见坑:和 mypy 的关系¶
很多童鞋会问:「ruff 能替代 mypy 吗?」
不能。
ruff 是 lint + formatter,做的是「代码风格、常见错误模式」检查。mypy 是类型检查器,做的是「这个变量传给那个函数,类型对不对」这种全局类型推导。两者是不同维度的工具,互补使用。
正确的姿势:
- 用
ruff管风格、import、明显 bug、语法升级 - 用
mypy或者pyright管类型
CI 上两个都跑:
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.toml 或 ruff.toml:
mono/
├── pyproject.toml # 根配置
├── service-a/
│ ├── pyproject.toml # service-a 自己的配置(可选)
│ └── ...
└── service-b/
├── ruff.toml # service-b 自己的配置
└── ...
ruff 在扫描每个文件时,会向上查找最近的配置文件。各子项目互不干扰。
一个进阶玩法:noqa 注释¶
有些时候你确实想让 ruff 闭嘴。比如某行代码里 eval() 用得有充分理由,但 S307(flake8-bandit)会告警。这时候可以加 # noqa 注释:
# noqa: S307 表示「这行代码忽略 S307 这条规则」。多条规则用逗号隔开:
裸的 # 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 同时写¶
这样配的意思是「启用 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」规则,默认不启用。要打开:
或者命令行 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 也跟着升。
小结¶
各位走完这一章,应该已经能:
- 装
ruff:uv add --dev ruff - 跑
ruff:ruff check --fix .和ruff format . - 配
ruff:pyproject.toml里写[tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM"] - 接
pre-commit:写.pre-commit-config.yaml,加ruff-pre-commit仓库 - 接
CI:GitHub Actions 用astral-sh/ruff-action@v3 - 接编辑器:VS Code 装
charliermarsh.ruff插件,配置保存时自动 format
这套工作流配好之后,新项目从零到「能自动检查、能自动修复、能 CI 兜底、能编辑器实时反馈」只需要十分钟。代码风格的事,交给 ruff 就行,各位省下来的精力可以用来写真正的业务代码。
下一章我们看 pytest——怎么写好 Python 项目的测试。uv 管依赖、ruff 管风格、pytest 管测试,这三件套配齐,2026 年的 Python 工程化就齐活了。