bigywater.tech
Python · 102

Ruff、Black 和 isort:Python 代码质量工具如何协作

Python 项目里常见一串:isort 排 import、black 排版、flake8/ruff 查问题。它们各管什么、放在一起会不会打架、ruff 能不能一个顶三个,这篇用同一段乱代码全部真跑了一遍,包括那个著名的「isort 与 black 互相不认」冲突。

1 → 0
isort 默认配置 → black --check 返回码;加 --profile black 后从 1 变 0
结果逐字相同
ruff(I)+ruff format 与 isort(black profile)+black 的输出
38x / 9x
本机 300 个文件上 ruff 相对 black / isort 的速度倍数

做的事:一段故意写乱的代码

messy.py(节选) import sys, os from typing import List,Dict, Optional import requests_not_installed from collections import (OrderedDict, defaultdict, Counter, deque, namedtuple, ChainMap, UserDict) import json import re def build(items:List[int], mapping: Dict[str,int]={}, flag = True)->Optional[str]: result = {'a':1,'b':2, 'c':3} ...
实测 · 本机执行输出
版本
版本: black 26.5.1; isort 9.0.2; ruff 0.16.9

验证一:各工具只做自己的事

实测 · 本机执行输出
  black                        改动 31 行 | 其中涉及 import 的 4 行 | 是否把 {'a':1 ...} 改成 {"a": 1, ...}: True
  isort                        改动 14 行 | 其中涉及 import 的 11 行 | 是否把 {'a':1 ...} 改成 {"a": 1, ...}: False
  ruff format                  改动 31 行 | 其中涉及 import 的 4 行 | 是否把 {'a':1 ...} 改成 {"a": 1, ...}: True
  ruff check --select I --fix  改动 21 行 | 其中涉及 import 的 11 行 | 是否把 {'a':1 ...} 改成 {"a": 1, ...}: False
  → black 与 ruff format 是「代码格式」,isort 与 ruff -I 是「import 排序」。谁也不会删掉没用到的 import:
    messy.py:1:8: F401 [*] `sys` imported but unused
    messy.py:1:13: F401 [*] `os` imported but unused
    messy.py:3:8: F401 [*] `requests_not_installed` imported but unused
    messy.py:4:26: F401 [*] `collections.OrderedDict` imported but unused
    messy.py:5:5: F401 [*] `collections.defaultdict` imported but unused
    messy.py:5:18: F401 [*] `collections.Counter` imported but unused
    messy.py:5:27: F401 [*] `collections.deque` imported but unused
    messy.py:5:34: F401 [*] `collections.namedtuple` imported but unused

black 与 ruff format 的改动几乎相同(31 行,其中动到 import 的只有 4 行——只是排版);isort 与 ruff 的 I 规则只动 import(isort 改 11 行 import,代码本体不动)。没有一个会删除没用到的 import:sys、os、requests_not_installed 等全都还在,想找出它们要靠 lint(ruff 的 F401,上面列出了前几条,可以加 --fix 自动删)。

验证二:isort 默认配置与 black 的冲突

实测 · 本机执行输出
  待处理的 import 片段:
    import sys, os
    from typing import List,Dict, Optional
    import requests_not_installed
    from collections import (OrderedDict,
        defaultdict, Counter, deque, namedtuple, ChainMap, UserDict)
    import json
    import re
  isort(默认配置) 之后, black --check 返回码 = 1 (1 = black 还想再改)
  isort 默认生成:
    import json
    import os
    import re
    import sys
    from collections import (ChainMap, Counter, OrderedDict, UserDict, defaultdict,
                             deque, namedtuple)
    from typing import Dict, List, Optional
    import requests_not_installed
  black 想改成:
    +from collections import (
    +    ChainMap,
    +    Counter,
    +    OrderedDict,
    +    UserDict,
    +    defaultdict,
    +    deque,
    +    namedtuple,
    +)
  isort --profile black 之后, black --check 返回码 = 0 (0 = 已经一致)

isort 默认把过长的 import 排成「网格」风格:续行缩进到括号位置;black 的风格是「一行一个名字 + 尾逗号」。所以先用默认配置的 isort,black 再检查一定说「还没格式化」。--profile black 让 isort 直接输出 black 风格,冲突消失(返回码 0)。

验证三:先后顺序与检查结果

实测 · 本机执行输出
  isort(默认) → black                black --check: 通过 | isort --check(默认配置): 失败 | isort --check(black profile): 通过
  black → isort(默认)                black --check: 失败 | isort --check(默认配置): 通过 | isort --check(black profile): 失败
  isort --profile black → black    black --check: 通过 | isort --check(默认配置): 失败 | isort --check(black profile): 通过
  black → isort --profile black    black --check: 通过 | isort --check(默认配置): 失败 | isort --check(black profile): 通过

四种流水线的最终状态:①isort 用默认配置时,无论先后,总有一个检查会失败:isort 在前则 isort --check 用默认配置时失败,isort 在后则 black --check 失败;②isort 用 --profile black,先后都能让 black 和 isort(black profile)两个检查通过。注意这里只是同一份样例的结果,配置项更多的项目里可能有别的冲突。

验证四:ruff 一个工具能顶两个吗

实测 · 本机执行输出
  isort(black profile)+black 与 ruff(I)+ruff format 的结果完全相同? True
  最终代码:
    import json
    import os
    import re
    import sys
    from collections import (
        ChainMap,
        Counter,
        OrderedDict,
        UserDict,
        defaultdict,
        deque,
        namedtuple,
    )
    from typing import Dict, List, Optional
    import requests_not_installed
    def build(items: List[int], mapping: Dict[str, int] = {}, flag=True) -> Optional[str]:
        result = {"a": 1, "b": 2, "c": 3}
        long_call = some_function_name(
            argument_number_one,
            argument_number_two,
            argument_number_three,
            argument_number_four,
        )
        if flag:
            return "yes"
        else:
            return "no"

在这份样例上,ruff check --select I --fix 加 ruff format 的结果和 isort --profile black 加 black 的输出完全相同(比较结果 True,最终代码见上)。这只说明这份样例上一致,不是所有代码都一致——两者的实现是独立的,遇到边缘情况(比如特殊注释、超长字符串)可能有差异,这里没有验证。

验证五:速度

实测 · 本机执行输出
  black                               918 ms
  ruff format                          24 ms
  isort --profile black               229 ms
  ruff check --select I --fix          26 ms
  (共 10200 行, 300 个文件; 每次都从原始文件重新拷贝, 取 3 次最小值)

300 个文件(共 10200 行):black 要 918 ms,ruff format 24 ms;isort 229 ms,ruff 的 import 排序 26 ms。差距在 CI 或者保存文件自动格式化时最容易感觉到。速度不是选工具的唯一理由,但配置只需要维护一份是实打实的好处。

分步演示:三类工作、一个冲突、一个替代

所有输出取自上面的真实运行
isort / black / ruff 如何协作
当前关注的代码 / 数据

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7;black 26.5.1、isort 9.0.2、ruff 0.16.9(临时虚拟环境);速度取 3 次最小值,每次从原始文件重新拷贝
    • 没有验证:项目配置文件(pyproject.toml 里的 [tool.*])的合并规则、ruff 的其他规则集(E、W、B 等)、pre-commit 集成、ruff 与 black 在更多语法(match、类型参数)上的输出差异
    京ICP备2026067502号-1京公网安备11010502063487号