
1. 为什么格式化这件事值得认真配置一次写 Python 的人大概都经历过这样的场景接手一个老项目打开某个文件缩进是 4 个空格和 Tab 混着的行尾有分号字符串引号一半单一半双函数之间空行数量全凭心情。你花十分钟改完一个函数git diff 里却显示改了三百行。这种痛感只有真正维护过别人代码的人懂。代码格式化工具要解决的就是这个问题。它把代码长什么样从人的主观判断变成一个可执行的确定性程序所有人保存文件的那一刻代码自动变成同一个样子评审的时候再也不用为空格数量吵架。VSCode 作为目前使用最广的编辑器之一和 Python 格式化工具体的集成已经非常成熟但配置这件事本身有个特点——上手十分钟配好要踩坑。工具链更新快Black、Ruff、isort、autopep8 各自定位不同VSCode 的settings.json、pyproject.toml、扩展默认值之间的优先级又容易打架很多人配完之后发现保存没反应或者格式化结果和命令行不一致。这篇内容就是围绕这件事展开的。我会从为什么要选某个工具、各工具的能力边界讲起然后给出一套可以直接抄的 VSCode 配置再聊速度对比、迁移路径、和 pre-commit、CI 的联动最后把我和身边同行踩过的坑整理成速查表。适合两类人看一类是刚把 VSCode 当成主力编辑器、想让保存即格式化的新手另一类是团队里负责统一代码风格、正在纠结要不要从 Black 切到 Ruff 的人。全部配置都经过实际运行验证参数和路径可以按自己项目调整。2. 工具选型的底层逻辑Black、Ruff、isort 各管什么2.1 先把格式化和导入排序分开看很多人一上来就问Black 和 Ruff 哪个好这个问题本身有点问题因为两者在能力范围上不完全对等。Black 是纯格式化器它只关心代码的排版缩进、换行、括号位置、字符串引号、尾随逗号。它有一个非常鲜明的立场——几乎没有配置项官方说法是你可以有的意见就是没有意见。这个设计初看很霸道实际用起来是优点团队里没人能通过改配置把自己的风格偏好塞进来。Ruff 定位是极快的 Python linter 加格式化器。它最早是作为 Flake8 的替代品出现的后来逐步吃掉了 isort、pyupgrade、部分 pylint 的规则再到 0.1 版本之后正式提供ruff format子命令对标 Black 的格式化能力。它的核心卖点是用 Rust 写的速度快一个数量级并且把 lint 和 format 收进同一个二进制。isort 是专门排 import 的。它处理的是标准库、第三方库、本地模块三段式分组、组内字母序、from x import y里名字的排序。Black 不管这个Ruff 通过I规则集可以完整替代它。所以三者关系是格式化排版这一层Black 和ruff format二选一导入排序这一层isort 和ruff的I规则二选一。混用可以但要清楚谁负责什么否则会出现两个工具互相改对方结果、保存一次代码变两次的诡异现象。2.2 我为什么建议新项目直接用 Ruff选型的时候我一般看三个维度速度、维护成本、生态兼容度。做个直观对比下面这张表是几个工具在中等规模项目大约 800 个 Python 文件上的粗略表现数字来自本机实测不同机器会有差异但量级关系稳定工具职责单次全量耗时粗略配置复杂度和 Black 格式兼容Black格式化8 到 12 秒极低几乎无配置基准ruff format格式化0.2 到 0.5 秒低配置集中在 pyproject高度兼容少量差异isort导入排序3 到 5 秒中配置项较多需与 Black 协同配置ruff check --fixlint 加导入排序0.1 到 0.3 秒中规则集选择需要理解不涉及这个差距在实际开发中的体感是Black 格式化一个大文件会有可感知的停顿半秒左右Ruff 基本是瞬时的。VSCode 的保存时格式化是个高频动作一天的保存次数可能上千次每次省 300 毫秒累积起来就是几分钟的注意力中断。当然这只是体验层面的理由更实际的理由是Ruff 一个工具同时管 lint、导入排序、格式化配置文件只有一份CI 里只需要装一个依赖团队让他人上手的成本低很多。那 Black 还有没有必要用有。存量项目、对格式极度敏感的项目、以及需要和上游开源项目保持一致的场景Black 依然是最稳妥的选择。它的格式化结果更保守社区沉淀时间长边界情况处理得更成熟。另外有些团队已经在 Black 上跑了几年格式化相关的 git blame 历史很干净切换工具会产生一次大规模 diff这个成本要算清楚。2.3 一个容易忽略的决策点格式化结果要不要进 CI配置格式化工具之前先想清楚一个问题格式化是建议还是强制。如果只是编辑器里保存时格式化那团队成员用别的编辑器、或者手动改格式就绕过去了同样的代码在不同机器上保存可能产生不同结果diff 噪音还是存在。我的做法是三层防线编辑器层VSCode 保存时自动格式化给即时反馈、提交层pre-commit 钩子阻断不合规提交、CI 层检查但通常不自动改只报错。编辑器和 CI 必须用同一个工具的同一套配置否则会出现我本地保存过了CI 还报格式错误这种最让人抓狂的情况。这个一致性问题后面会专门讲它是配置翻车的第一大原因。3. VSCode 侧的核心配置让保存即格式化真正生效3.1 扩展选择与安装顺序VSCode 里 Python 相关的扩展现在有点乱先理清楚装哪些Python 扩展微软官方提供语言服务器 Pylance、调试、环境选择。它内置了一些格式化入口但本身不提供格式化能力。Ruff 扩展charliermarsh 那个提供ruff check的诊断和ruff format的格式化入口。装完之后在命令面板能搜到 Ruff: Format。Black Formatter 扩展如果你用 Black装这个。注意它是独立扩展和 Python 扩展是两回事。安装顺序其实不影响功能但有个细节要注意如果你同时装了 Black Formatter 和 Ruff 扩展VSCode 会有两个格式化提供者默认行为取决于editor.defaultFormatter的设置没设的话可能弹窗让你选也可能随机用其中一个。这正是很多人保存后格式没变的根源。提示装完扩展后先按CtrlShiftPmacOS 是CmdShiftP输入 Format Document With看看列表里有几个格式化器再决定用哪个。这个动作比翻文档快得多。3.2 用户设置还是工作区设置VSCode 的设置有三个层级默认设置、用户设置全局、工作区设置项目内的.vscode/settings.json。格式化相关配置强烈建议放工作区理由很直接不同项目用的工具可能不同老项目用 Black、新项目用 Ruff如果写在用户设置里切换项目就会互相干扰。工作区设置的优先级高于用户设置这个机制可以用来做全局兜底加项目覆盖用户设置里开editor.formatOnSave这个通用开关具体用哪个格式化器、传什么参数交给每个项目的.vscode/settings.json。这样新克隆一个仓库打开就能直接用不用问同事你用的什么格式化。这里有个实践中的坑.vscode/settings.json要不要进版本库要进。它是团队配置的一部分和pyproject.toml一样应该被 review。但.vscode/目录下常有个人东西比如launch.json里的本地路径所以常见做法是提交settings.json和extensions.json把launch.json加进.gitignore。extensions.json里写recommendations新同事打开项目时 VSCode 会提示装 Ruff 扩展这一步能省很多沟通。3.3 一套可以直接抄的 settings.json下面是我现在新项目在用的配置用 Ruff 做格式化和 lint。逐行都能解释为什么{ [python]: { editor.defaultFormatter: charliermarsh.ruff, editor.formatOnSave: true, editor.codeActionsOnSave: { source.fixAll.ruff: explicit, source.organizeImports.ruff: explicit } }, ruff.organizeImports: true, ruff.fixAll: true, editor.rulers: [88], files.trimTrailingWhitespace: true, files.insertFinalNewline: true }几个关键点逐条说。editor.defaultFormatter指定具体扩展 ID不要留空留空就是让 VSCode 自己猜猜的结果不可控。editor.formatOnSave是总开关。codeActionsOnSave里的source.fixAll.ruff会执行ruff check --fix把能自动修的问题都修掉source.organizeImports.ruff单独管导入排序。注意这里的值写的是explicit而不是true这是 VSCode 较新版本的要求写true会有警告语义是只在显式保存时执行不在自动保存时执行避免自动保存频繁触发全量修复。editor.rulers设成 88 是配合 Black 和 Ruff 默认的行宽。这个数字要和pyproject.toml里的line-length对齐否则编辑器里显示一条竖线在 88 列实际格式化按 100 列来看着就难受。files.trimTrailingWhitespace和files.insertFinalNewline是编辑器层的兜底虽然格式化器也会处理但这两个对非 Python 文件同样有效属于零成本的好习惯。如果你用 Black把[python]那一段换成{ [python]: { editor.defaultFormatter: ms-python.black-formatter, editor.formatOnSave: true, editor.codeActionsOnSave: { source.organizeImports: explicit } } }导入排序这里就得靠 isort 扩展或者 Ruff 的 lint 能力来补了因为 Black 自己不管 import 顺序。这就是前面说的混用要清楚谁管什么。3.4 pyproject.toml 里的对应配置VSCode 的 settings 只管用哪个工具、什么时候触发工具自己的行为由项目配置文件决定。Ruff 的配置集中写在pyproject.toml[tool.ruff] line-length 88 target-version py311 exclude [migrations, build, .venv] [tool.ruff.lint] select [E, F, I, UP, B, SIM] ignore [E501] [tool.ruff.format] quote-style double indent-style space skip-magic-trailing-comma falseline-length 88是 Black 的默认值保持一致可以让两个工具的产出接近。target-version建议明确写出来Ruff 会据此决定哪些语法可以自动升级比如把旧式格式化字符串提示成 f-string不写的话它从文件里的语法特征推断偶尔会保守。select里I就是导入排序规则这行配置的等价物就是 isortE501在ignore里是因为行宽已经由格式化器负责了lint 再报一遍是重复噪音这个取舍很多人不知道导致编辑里一堆波浪线。注意 Ruff 较新版本把 lint 和 format 的配置拆成了[tool.ruff.lint]和[tool.ruff.format]老的[tool.ruff]下直接写select会报弃用警告。升级 Ruff 之后如果配置报错先看这个。4. 完整实操流程从零配到一个能跑的样板4.1 环境准备与版本确认先确认几个版本版本不匹配是配置失效的常见原因python --version # 建议 3.10 以上 ruff --version # 建议 0.6 以上0.4 之前 format 子命令还不稳定Ruff 通过 pip 安装pip install ruff或者用uv tool install ruff之类的隔离方式。装完之后必须确认 VSCode 用的是哪个 Python 环境命令面板执行 Python: Select Interpreter选到项目虚拟环境里那个。Ruff 扩展默认会在工作区找ruff可执行文件找不到会提示这时可以在 settings 里显式指定{ ruff.importStrategy: fromEnvironment, ruff.interpreter: [${workspaceFolder}/.venv/bin/python] }interpreter这个配置项在 Windows 和 macOS/Linux 上路径不同Windows 是.venv\Scripts\python.exe跨平台团队要注意这个差异或者干脆把这个路径排除在提交之外让大家各自配本地路径只提交importStrategy。4.2 建立基准先格式化一次全量代码配置之前先做一件事在干净的工作分支上对全项目跑一次全量格式化单独提交。这一步的意义是把纯格式变更和业务变更分开否则后续任何一次业务提交里都会混着大量格式 diffreview 的人根本看不出逻辑改动。git checkout -b chore/format-baseline ruff format . ruff check --fix --select I . git add -A git commit -m chore: apply ruff format baseline全量格式化之后大概率有几百个文件被改。这个提交要让团队里的所有人都知道因为所有人本地分支合并上来都会有冲突。经验做法是选一个业务需求空窗期比如周五下午提前一天通知格式化当天大家只做小改动合并完再继续。冲突解决的原则是格式冲突直接接受格式化的结果业务逻辑冲突手工处理这个规则提前说清楚能省掉很多来回。4.3 逐文件验证保存格式化全量基线建好之后验证单文件行为。打开任意一个.py文件故意把缩进改成两个空格、在行尾加几个空格然后CtrlS。预期结果是缩进恢复成四个空格、行尾空格被清掉。如果没反应按顺序排查三件事右下角状态栏看当前文件的语言模式是不是 Python有时候被识别成 Plain Text格式化器不会触发。命令面板执行 Format Document看是否弹窗提示没有安装格式化程序这通常意味着扩展没装或未启用。检查输出面板选 Ruff看有没有报错配置语法错误会让整个扩展静默失效。这三步覆盖了九成以上的保存不格式化问题。剩下的一成通常是保存方式的问题VSCode 的自动保存files.autoSave和保存时格式化是两套逻辑codeActionsOnSave里写explicit就是为了避免自动保存时频繁触发修复动作导致卡顿如果你希望自动保存也格式化那需要另外调editor.formatOnSaveMode但一般不建议体感很糟。4.4 参数选择行宽、引号、尾随逗号的取舍这几个参数看起来琐碎实际影响最大因为它们是 diff 噪音的主要来源。行宽line-lengthBlack 和 Ruff 默认都是 88。这个数字不是随便定的它来自 Black 作者对在不影响可读性的前提下屏幕能放几列代码的统计考虑到左右分栏和 diff 视图。如果团队都习惯宽屏单栏可以调到 100 或 120但一旦定了就不能改改一次就是全项目 diff。我的建议是能不调就不调88 是生态里兼容性最好的值很多开源库的配置也是 88复制粘贴代码进来不用重排。引号风格quote-styledouble是 Black 系的默认。Ruff 提供了quote-style single的选项但选 single 之后要注意和 Black 的兼容性会变差如果项目里还有 Black 的 CI 检查两边会打架。纯 Ruff 项目倒是随意看团队习惯。尾随逗号skip-magic-trailing-comma这个配置控制的是当调用参数被拆成多行时是否因为最后有个逗号就强制保持多行。设成false默认即不跳过 magic trailing comma意味着你写了多行加尾随逗号格式化器就保持多行你删掉逗号它可能压缩成一行。这个行为在多参数函数调用时很好用想强制多行就在最后加个逗号格式化器会尊重你。理解了这个机制你就知道怎么手动控制格式化器的展开了。4.5 和 pre-commit 打通编辑器层只对在 VSCode 里保存生效命令行git commit或者别的编辑器绕过它。加一层 pre-commitrepos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.9 hooks: - id: ruff args: [--fix] - id: ruff-formatrev这个版本号必须和本地安装的 Ruff 版本大致对齐。pre-commit 会自己下载指定版本的 Ruff 到独立环境里跑如果本地是 0.6 而钩子是 0.4两边格式化结果可能有差异表现就是本地保存没问题提交时被改了。这种问题很难查因为报错信息只是说文件被修改。钩子执行失败时 git commit 会被阻断此时文件已经被格式化过了git add之后重新 commit 即可。这个流程第一次遇到会有点懵习惯了就好它保证了进仓库的代码一定是格式化过的。注意pre-commit 钩子在首次运行时需要下载环境可能卡住一两分钟这不是 bug。团队新人第一次提交时经常以为是网络问题提前说一句能省很多困惑。5. 从 Black 迁移到 Ruff真实操作路径和踩过的坑5.1 迁移之前先做兼容性评估Black 和ruff format的格式化结果高度接近但不是完全相同。差异集中在少数边界情况字符串拼接的换行处理、某些复杂表达式的括号放置、注释位置。Ruff 官方给了一个专门的做法用ruff format --diff在存量代码上跑一遍看有多少处会和当前格式不同。ruff format --diff . | head -100 ruff format --diff . | grep -c ^---先看几百行 diff 长什么样心里有数。如果项目已经用 Black 格式化过差异通常只有总文件的 1% 到 5%。这个 diff 量决定了迁移成本差异小一次性切换差异大比如项目里有大量长字符串或复杂链式调用可以先在 CI 里允许两种格式并存一段时间或者接受一次大 diff。5.2 分步迁移的具体做法我推荐的顺序是这样的每一步都是独立提交方便回滚先切 lint把 flake8、isort、pyupgrade 这些 lint 工具换成 Ruff 的ruff check格式化仍然用 Black。这一步风险最低因为 lint 报错是可见的改不改可以逐个判断。再切格式化Black 换成ruff format。切换前跑一次ruff format --diff存档切换后跑一次全量格式化单独提交。清理依赖从requirements-dev.txt里删掉 black、flake8、isort加上 ruff。同时更新 pre-commit 配置和 CI 脚本。更新编辑器配置改.vscode/settings.json的defaultFormatter让扩展推荐列表加上 Ruff删掉 Black Formatter 的推荐。第 2 步之后要特别验证一件事CI 里的格式检查是不是真的在用 Ruff。很多项目 CI 脚本里写的是black --check .切换工具时只改了本地配置忘了改 CI结果本地保存格式化好的代码推上去 CI 报错报的还是 Black 的错误。这种低级错误我见过不止一次检查清单里应该有一条全局搜索项目里的 black 关键字。5.3 迁移后必须保留的配置项切到 Ruff 之后有些 Black 的行为需要显式配置才能保持容易漏Black 行为Ruff 对应配置说明行宽 88line-length 88默认值一致但如果 Black 时改过要同步双引号quote-style double默认就是 double尾随逗号保留skip-magic-trailing-comma false默认 false若 Black 时用了--skip-magic-trailing-comma要注意单行 if 不合并无直接对应Ruff 更倾向于保持原样这点反而更保守目标版本target-version py311Black 从 pyproject 的 requires-python 推断Ruff 建议显式写最后一行值得多说一句。Black 会读pyproject.toml里的requires-python来推断目标版本Ruff 也支持这个推断但推断失败时会退回到一个保守的默认值表现就是某些能升级的语法它不敢升。显式写target-version可以避免这种不确定性。5.4 团队协作中的推行技巧工具切换是人的问题不是技术问题。我踩过的坑里最难的不是配置是让团队接受一次几百文件的 diff。几个实操经验提前准备一份为什么换的一页纸。别写长篇大论就三点格式化速度提升到什么程度给具体数字、依赖从几个减到一个、配置从几份合成一份。工程师对速度和依赖数量的敏感度远高于工具更现代这种说法。把全量格式化和业务分支错开。前面提过补一句细节格式化提交要放在业务分支合并之后、新需求开始之前。如果团队用 trunk-based 开发那就选一个大家都在的低活跃时段格式化完立刻通知。给不愿意换的人留退路。有人就是习惯 Black可以让他本地继续用但 pre-commit 和 CI 用 Ruff 卡住。听起来矛盾实际可行ruff format和 Black 的产出在 95% 以上的代码上一致剩下的差异由 pre-commit 兜底修正。这不是理想状态但比强行要求所有人同一天切换要好。观察一周再删旧工具。切完之后别急着删 Black 的依赖和配置留一周。这一周里如果发现 Ruff 有某个行为团队实在不能接受回滚成本最低。一周后再清理同时更新 README 里的开发说明——这一步最容易被忘新人照着旧文档装 Black装完发现没用浪费时间。6. 常见问题与排查速查表6.1 保存不格式化按这个顺序查现象可能原因排查动作保存完全没反应语言模式不对右下角确认是 Python保存没反应没设 defaultFormatter命令面板 Format Document With 看列表弹窗让选格式化器装了多个格式化扩展显式设editor.defaultFormatter只格式化了部分文件有语法错误看输出面板 Ruff 日志格式化后又被改回去两个格式化器打架禁用其中一个扩展保存很卡codeActionsOnSave设了 true改成explicit倒数第二条值得展开。格式化后又被改回去的典型表现是按保存代码变了一下再按保存又变回去来回横跳。原因是两个格式化提供者比如 Black 和 Ruff对同一段代码有不同意见VSCode 每次保存调用其中一个另一个的保存时修复又把它改回来。解决办法只有一个只留一个格式化器。在扩展面板里禁用另一个或者在 settings 里明确指定别依赖默认行为。6.2 本地和 CI 结果不一致这是最高频的求助。表现本地保存看起来格式化好了推上去 CI 报格式错误。原因通常有四类版本不一致。本地 Ruff 是 0.6.9CI 装的是ruff最新版或者 pre-commit 钩子 pin 的是老版本。格式化器的行为在小版本间会有调整。解决办法是把版本 pin 死requirements-dev.txt里写ruff0.6.9pre-commit 的rev写同一个版本CI 里也装这个版本。三处对齐问题消失。配置没被读到。Ruff 从当前工作目录向上找pyproject.toml。如果 CI 在子目录里执行命令可能读不到根目录的配置。加--config pyproject.toml显式指定或者确认 CI 的工作目录。编辑器和命令行用的不是同一个可执行文件。本地可能装了多个 Ruff系统级、虚拟环境、pipxVSCode 用的和终端里which ruff指向的不是一个。用ruff --version在 VSCode 集成终端里跑一次对比一下。排除规则不一致。pyproject.toml里exclude排除了某些目录但 VSCode 的files.exclude没同步或者反过来。表现是某个文件本地不格式化、CI 却检查它。这个要靠 review 配置文件保证一致。6.3 格式化破坏了代码语义的几种罕见情况格式化器理论上不该改变语义但有几个边界值得知道字符串里的隐式拼接。多行字符串用括号隐式拼接时格式化器可能调整换行位置虽然字符串内容不变但如果那段字符串是 SQL 或者正则换行位置变化可能影响可读性甚至某些场景下的行为比如依赖行号的日志解析。这种情况可以用# fmt: off和# fmt: on包起来Ruff 和 Black 都支持这个注释。类型注解里的字符串前向引用。格式化器会调整引号风格如果代码里有ClassName这种字符串形式的注解改成双引号没问题但如果原本用的是单引号且代码里有转义要检查一下。注释和代码的相对位置。格式化器有时会把行尾注释挪到上一行或下一行。如果注释是给某一行特定代码的比如# noqa位置变了可能失效。# noqa、# type: ignore这类有语义的注释格式化后要抽查。提示项目里如果有# fmt: off的区块建议在 README 里说明为什么这块不格式化否则后来的人看到会想顺手格式化一下然后引入非预期 diff。6.4 性能相关的实际体感Ruff 快是共识但具体到 VSCode 保存这个场景有几个观察首次打开大项目时Ruff 扩展会做一次全量扫描这时保存可能有一两秒延迟之后就正常了。这个延迟和项目大小相关几千文件的项目首次扫描可能要十几秒扫描期间编辑器里会有诊断信息陆续出现别慌这是正常的。codeActionsOnSave里同时开fixAll和organizeImports时Ruff 会跑两遍 lint一次修全部一次排 import。可以合并成只开fixAll因为 Ruff 的I规则在fixAll里也会被执行——前提是select里包含I。这个小优化能减少一次全文件扫描在大文件上体感明显。另外如果项目里有超长的单行文件比如自动生成的 protobuf 或数据文件格式化会很慢甚至卡住。这类文件应该加进exclude别让格式化器碰。7. 我个人的几条经验配置这件事有个反直觉的地方越简单的配置越稳定。我见过有人把settings.json写成两百行每个语言单独配格式化器、每个格式化器带一堆参数最后自己都说不清哪个生效。Ruff 的一个隐性优势就是它逼你收敛一份pyproject.toml一个defaultFormatter加两行codeActionsOnSave就完了。剩下的交给工具的默认值。关于 Black 和 Ruff 的选择我的实际体会是新项目无脑 Ruff存量项目别急着换。存量项目换工具的收益主要是速度但代价是一次大 diff 和团队心智负担。如果团队规模小、代码量不大这个收益其实没那么显著Black 稳定跑了几年也是一种资产。真正该换的信号是CI 跑得慢到影响迭代节奏、或者 lint 和格式化用的工具太杂导致配置维护成本高。最后分享一个小技巧。迁移或者大改格式化配置之前先在项目根目录跑一次ruff format --diff . format-before.diff把当前状态存下来。改完配置再跑一次生成format-after.diff对比两份文件的差异你能精确看到配置改动的实际影响范围。这一步比看文档猜效果靠谱得多尤其在调line-length和skip-magic-trailing-comma这类参数时diff 会直白地告诉你代价是多少行。养成这个习惯之后我对格式化配置的每次改动都心里有数不会出现改了个参数整个 repo 红了的情况。