Pylint 2.5 版本新特性与检查器深度解析

原创2026-10-12 14:14:491,836 阅读
文章标签:静态分析代码质量Lint开发工具

Pylint 2.5 版本新特性与检查器深度解析

导读

Pylint 2.5(发布于 2020-04-27,配套 2.5.1 / 2.5.2 / 2.5.3 三个修复版本)是一次功能密度极高的版本迭代:一次性新增了十余个检查器、引入 setup.cfg / pyproject.toml 配置读取能力、增加 --fail-under、notes-rgx、good-names-rgxs / bad-names-rgxs 等关键配置项,并修复了大量误报(false positive)与漏报(false negative)问题。本文以 doc/whatsnew/2/2.5/index.rst 所统领的发布摘要与完整变更日志为主体,结合当前仓库源码逐项印证这些新特性的实现细节、默认值与使用方式,帮助你在升级或配置 Pylint 时快速理解 2.5 引入的行为变化。

说明:index.rst 为版本说明的索引页,其主体内容位于同目录的 summary.rst(发布亮点摘要)与 full.rst(完整变更日志),本文完整覆盖二者内容。

一、2.5 版本总览

Pylint 2.5.0 于 2020-04-27 发布,随后 2.5.1、2.5.2(均为 2020-05-05)与 2.5.3(2020-06-08)陆续修复回归问题。本次迭代的核心主题可以概括为:

  • 新增一批“一眼就能看出 bug”的检查器:isinstance 第二参数类型校验、字符串字面量 assert、无插值 f-string、引号风格一致性、__name__ 赋值类型校验、特殊方法(协议函数)返回类型校验等;
  • 配置能力扩展:支持从 setup.cfg 与 pyproject.toml 读取配置、正则化命名白名单/黑名单、notes-rgx 正则化 FIXME 标记;
  • CI 集成强化:新增 --fail-under 评分阈值,让 Pylint 可直接作为测试门禁;
  • 安全与命名规范调整:python -m pylint 不再把当前目录注入 sys.path、非 ASCII 标识符单列检查。

这些内容在 summary.rst 中有完整记录,下文将逐一展开并结合源码佐证。

二、新增检查器详解

2.1 isinstance-second-argument-not-valid-type(E1121 区域)

功能:检测 isinstance(obj, x) 中第二个参数不是类型的情况。此类代码几乎必然是笔误——运行时必然抛出 TypeError。

触发示例(来自 doc/data/messages/i/isinstance-second-argument-not-valid-type/bad.py):

isinstance("apples and oranges", hex)  # [isinstance-second-argument-not-valid-type]

实现依据:该检查位于 pylint/checkers/typecheck.py,由 _check_isinstance_args 在 visit_call 中触发:当被调函数名为 isinstance 且参数少于 2 个时提示 no-value-for-parameter,多于 2 个时提示 too-many-function-args,恰好 2 个参数时则用 _is_invalid_isinstance_type 校验第二参数是否为合法类型。

从 _is_invalid_isinstance_type(typecheck.py#L870-L892)的实现可以看出其判定逻辑相当细致:

  • 合法的第二参数包括:ClassDef(类对象)、推断为 builtins.tuple 的实例(即 (int, str) 这样的类型元组)、X | Y 联合类型表达式(PEP 604 联合)以及 UnionType;
  • 只要推断结果不是上述类型之一,即判定为“不是合法类型”;
  • 无法推断时(safe_infer 返回空)选择跳过,避免误报。

这意味着 isinstance(x, (int, str)) 不会触发,而 isinstance(x, "str")、isinstance(x, 42)、isinstance(x, some_var)(当 some_var 不是类型时)都会被报告。

2.5 中还修复了 __class__ 使用场景下的 undefined-variable 误报(issue #3090),以及类属性在推导式/装饰器/类型注解中的 undefined-variable 误报(issue #3494、#511、#1976),均发生在 typecheck.py 与 variables.py 涉及的推断链路上。

2.2 assert-on-string-literal(E0120 区域)

功能:检测 assert 语句的第一个参数是字符串字面量的情况。这类断言恒为真(非空字符串)或恒为假(空字符串),属于几乎必然的编码错误。

触发示例(来自 doc/data/messages/a/assert-on-string-literal/bad.py):

def test_division():
    a = 9 / 3
    assert "No ZeroDivisionError were raised"  # [assert-on-string-literal]

实现依据:实现在 pylint/checkers/base/basic_checker.py#L731-L739 的 visit_assert 中,与已有的 assert-on-tuple 检查共用同一个入口:若 assert 的测试表达式是含元素的元组则报 assert-on-tuple(历史上这是 assert (x, y) 写成元组的经典误用);若是字符串常量则报 assert-on-string-literal,并且通过 when = "never" if val else "always" 区分空字符串与非空字符串,在消息参数中告知该断言“永不触发”或“恒成立”。

2.3 f-string-without-interpolation(W1309)

功能:检测没有任何插值变量的 f-string。既然没有 {} 插值,f"..." 就应当退化为普通字符串——要么是冗余写法,要么是忘记写 {} 的 bug。

实现依据:消息定义位于 pylint/checkers/strings.py#L184-L189(W1309),检查逻辑由 only_required_for_messages("f-string-without-interpolation") 装饰的处理函数触发(strings.py#L409-L419)。同家族的 format-string-without-interpolation(W1310)也被引入,覆盖 "{}".format() 风格。

仓库中对应的功能测试见 tests/functional/f/f_string_without_interpolation.py 与同目录 .txt 期望输出。

2.4 协议函数返回类型检查(E0304–E0313)

功能:一次性新增 10 个检查器,用于校验特殊方法(协议方法)的返回类型是否合法。列表如下(消息 ID 与符号名在 full.rst 中有明确标注):

消息 ID 符号名 检查的方法 期望返回类型
E0304 invalid-bool-returned __bool__ bool
E0305 invalid-index-returned __index__ 整数
E0306 invalid-repr-returned __repr__ 字符串
E0307 invalid-str-returned __str__ 字符串
E0308 invalid-bytes-returned __bytes__ 字节串
E0309 invalid-hash-returned __hash__ 整数
E0310 invalid-length-hint-returned __length_hint__ 非负整数
E0311 invalid-format-returned __format__ 字符串
E0312 invalid-getnewargs-returned __getnewargs__ 元组
E0313 invalid-getnewargs-ex-returned __getnewargs_ex__ (tuple, dict) 形式的二元组

实现依据:全部定义在 pylint/checkers/classes/special_methods_checker.py#L88-L141,并通过 _protocol_map 字典将方法名映射到对应检查函数(special_methods_checker.py#L146-L161)。visit_functiondef 中先调用 _safe_infer_call_result 推断返回值,仅在“能推断出类型”时才做检查,避免因推断失败而误报。

几个值得注意的实现细节:

  • __bool__、__index__、__hash__ 等通过 _is_int / _is_bool 做“子类兼容”判断(bool 也是 int 的子类,IntEnum 成员可按整数处理);
  • __length_hint__ 与 __len__ 一样要求非负整数,推断出常量且为负时同样报警;
  • __getnewargs_ex__ 要求返回 (tuple, dict) 形状,若推断结果是 nodes.Tuple 还会进一步逐个元素校验(第一元素须为元组、第二元素须为字典)。

配套功能测试位于 tests/functional/i/invalid/ 目录,每个检查器都有独立的 .py 与 .txt 期望输出,例如 invalid_bool_returned.py、invalid_getnewargs/invalid_getnewargs_ex_returned.py。

2.5 inconsistent-quotes(W1405)

功能:检测整个模块内字符串引号定界符(" 与 ')使用不一致的情况。它的设计目标很实用:允许为了“避免不必要的转义”而例外——例如某个模块整体使用单引号定界,但 "Don't error" 中的单引号会与定界符冲突,此时允许用双引号而无需转义。

配置前提:该检查默认关闭,必须显式打开 check-quote-consistency 选项(源码见 strings.py#L694-L704,类型为 yn,默认 False)。示例配置(来自 doc/data/messages/i/inconsistent-quotes/pylintrc):

[main]
check-quote-consistency=yes

触发示例(来自 doc/data/messages/i/inconsistent-quotes/bad.py):

import datetime

print('Current year: ', datetime.date.today().strftime("%Y")) # [inconsistent-quotes]

实现依据:消息定义 W1405 位于 strings.py#L667-L672,实现依托 process_tokens 对 tokenize.STRING 令牌的逐行扫描,统计每行使用的主要引号风格,并在不一致处报告(strings.py#L873)。功能测试见 tests/functional/i/inconsistent/ 目录。

2.6 non-str-assignment-to-dunder-name

功能:确保 __name__ 属性只被赋值字符串。例如 self.__name__ = 42 或 SomeClass.__name__ = ["x"] 这类赋值会被报告。

实现依据:位于 pylint/checkers/typecheck.py 的 _check_dundername_is_string(typecheck.py#L1445-L1464):先确认赋值左侧是 <something>.__name__(AssignAttr 且 attrname == "__name__"),再检查右侧:直接是字符串常量则放行;否则通过 safe_infer 推断,只要推断结果不是 Const 且值非字符串即报告。测试见 tests/functional/n/non/non_str_assignment_to_dunder_name.py。

2.7 non-ascii-name(C2401)与命名规则调整

背景:2.5 同时做了两项关联调整:

  1. invalid-name 不再对非 ASCII 标识符一刀切报错(见下文“其他变更”),
  2. 新增独立的 non-ascii-name 检查,专门提示“标识符含非 ASCII 字符,建议重命名”——这是因为不少 Unicode 字符与 ASCII 字符外观相似(混淆字符),容易引发阅读与维护问题。

实现依据:实现在独立的 pylint/checkers/non_ascii_names.py(NonAsciiNameChecker,消息 C2401)。该检查器只检查标识符名称,忽略 docstring 与注释内容;同时还包含 non-ascii-file-name(W2402)用于提示文件名中的非 ASCII 字符。测试覆盖非常全面,见 tests/functional/n/non_ascii_name/ 与 tests/functional/n/non_ascii_name_class/ 目录。

三、配置与命令行新能力

3.1 从 setup.cfg / pyproject.toml 读取配置

2.5 起,Pylint 不再局限于 .pylintrc,可以直接从当前目录的 setup.cfg 或 pyproject.toml 读取配置,也可在命令行显式传入这些文件。要点(summary.rst):

  • setup.cfg:section 名须加 pylint. 前缀,例如 [pylint.MESSAGES CONTROL];
  • pyproject.toml:section 名须加 tool.pylint. 前缀,例如 [tool.pylint.'MESSAGES CONTROL']。

这与当前仓库的配置解析架构一脉相承:配置加载统一由 pylint/config/config_file_parser.py 与 config_initialization.py 处理,官方示例见 examples/pyproject.toml([tool.pylint.main] 区块内直接使用 fail-under = 10 等键值)。2.5.3 还修复了 TOML 配置中只能使用字符串的问题:现在 enable、disable 等选项支持原生 list / int / bool 类型,例如:

enable = [
    "use-symbolic-message-instead",
    "useless-suppression",
]

3.2 正则化命名白名单/黑名单:good-names-rgxs 与 bad-names-rgxs

在原有 good-names / bad-names(逗号分隔的固定名单)基础上,2.5 新增正则版本:

  • good-names-rgxs:逗号分隔的正则列表,匹配任一正则的名称即豁免于命名检查;
  • bad-names-rgxs:逗号分隔的正则列表,匹配任一正则的名称一律视为非法命名。

实现依据:选项定义在 pylint/checkers/base/name_checker/checker.py#L258-L287,类型为 regexp_csv,默认值为空字符串。与之配套的固定名单默认值也值得注意:

  • good-names 默认:("i", "j", "k", "ex", "Run", "_");
  • bad-names 默认:("foo", "bar", "baz", "toto", "tutu", "tata")。

也就是说,2.5 中 foo / bar / baz 等占位名默认就会被 invalid-name 标记;如果你有自定义的前缀约定(如 temp_*、tmp*),可以用 good-names-rgxs 放行。

3.3 --fail-under:把评分变成 CI 门禁

新增 --fail-under <score>,用于设定“整体评分阈值”:若最终评分高于该阈值,Pylint 以退出码 0 结束(视为成功);否则以基于已发消息计算的现有退出码结束(视为失败)。

实现依据:选项定义在 pylint/lint/base_options.py#L155-L163,类型为 float,默认值 10(即默认要求满分才通过)。既可作为命令行参数,也可写入 .pylintrc / pyproject.toml:

# .pylintrc
[main]
fail-under=10
# pyproject.toml
[tool.pylint.main]
fail-under = 10

运行时逻辑在 pylint/lint/pylinter.py#L1297:当评分低于阈值时,评分输出会附带 -- below fail-under threshold (X) 提示。2.5 的配套修复还包括:2.5.3 修复了多任务(jobs)并行时评分不被报告的问题(issue #3547),2.6 又修复了 fail-under 不接受浮点数的问题——使用旧版本时建议显式写浮点形式。

3.4 notes-rgx:正则化 FIXME 标记

原有 notes 选项是逗号分隔的标记列表(默认 ("FIXME", "XXX", "TODO"))。2.5 新增 notes-rgx,允许用正则表达式定义要检测的标记,两者可同时生效。

实现依据:定义在 pylint/checkers/misc.py#L84-L92,类型为字符串,默认 ""。运行期将 notes 列表与 notes-rgx 拼接成正则后匹配注释与(可选)docstring(misc.py#L107-L120)。例如允许“除特定编号外均报警”的写法(参考 tests/functional/f/fixme.rc):

notes-rgx=FIXME(?!.*ISSUE-\d+)|TO.*DO

注意 fixme 检查还受 check-fixme-in-docstring(默认 False)控制是否扫描 docstring。

3.5 --list-extensions:列出全部扩展

新增命令 --list-extensions,输出 pylint.extensions 包中所有可用扩展的导入名。

实现依据:实现在 pylint/config/callback_actions.py#L210-L222(_ListExtensionsAction):遍历 pylint.extensions.__file__ 所在目录,输出所有不以 _ 开头的 .py 模块,格式为 pylint.extensions.<name>,随后以退出码 0 结束。例如:

$ python -m pylint --list-extensions
pylint.extensions.bad_builtin
pylint.extensions.broad_try_clause
pylint.extensions.check_elif
...

仓库中的扩展清单可对照 pylint/extensions/ 目录查看。

3.6 其他配置与行为变更汇总

  • 可变 collections.* 默认值视为危险:dangerous-default-value 现在会把 collections.deque([]) 等可变集合(不只 list/dict/set)标记为危险默认值(issue #3183);
  • 模块级非常量不再报 invalid-name:此前 Pylint 把“模块级变量都是常量”理解得过宽,2.5 起仅对模块级循环内定义的变量等场景保留 invalid-name(issue #2695、#3111、#3132);
  • python -m pylint 不再导入用户代码:因为 python -m 会把当前目录注入 sys.path 首位,存在安全风险(恶意/同名 stdlib 模块可能被导入),2.5 起移除了该行为(issue #3386);
  • logging-fstring-interpolation 回归:logging-format-style 选项不再决定允许的格式化风格,改为通过启用/禁用消息来控制——fstr 值不再有效,.format / % 风格各自对应独立消息(issue #3361);
  • overload 函数豁免:带 @typing.overload 的函数不再触发 no-self-use、unused-argument、missing-docstring 等面向普通函数的检查;Ellipsis 也从 multiple-statements 中豁免(issue #3224);
  • JSON 输出不再转义 HTML(issue #2769);json 报告器不再绕过 redirect_stdout(issue #3227)。

四、2.5.x 补丁版本修复要点

2.5.1(2020-05-05)

  • 修复未知基类下 method-hidden 的崩溃(issue #3527);
  • pylint.Run 的退出参数恢复为 do_exit(此前被误改为 exit,issue #3533);
  • no-value-for-parameter 对赋值语句的可变参数检测改进(issue #3563);
  • 多任务模式下包文件可被正确发现(issue #3524);
  • 允许直接 lint 没有 __init__.py 的目录(修复 2.5 的回归,issue #3528)。

2.5.2(2020-05-05)

  • pylint.Run 接受 do_exit 作为已弃用参数(issue #3590)。

2.5.3(2020-06-08)

  • 修复含数字的 checker 名称在 # pylint: disable= 注释中解析失败的回归(issue #3666);
  • property-with-parameters 正确处理抽象属性(issue #3600);
  • Python 3.8 上 continue-in-finally 不再误报(3.8 起该写法合法,issue #3612);
  • 修复含连字符消息解析不完整的回归(issue #3604);
  • TOML 配置支持原生 list/int/bool 类型(issue #3538);
  • 修复多任务下评分不报告的回归(issue #3547);
  • cell-var-from-loop 检查增加 AttributeError 防护(issue #3646)。

五、升级与实战建议

  1. 先看消息清单:升级后运行 python -m pylint --list-msgs-enabled 与 python -m pylint --list-extensions,确认新增检查器与可用扩展;新增检查器(如 invalid-*-returned 系列)默认启用,首次接入可能出现新增告警,建议在 CI 中先以 --disable=invalid-bool-returned,... 过渡观察。
  2. 配置迁移:若项目已用 pyproject.toml,按 [tool.pylint.MESSAGES CONTROL] 等带 tool.pylint. 前缀的 section 组织;若用 setup.cfg,则用 [pylint.MESSAGES CONTROL] 形式(参考 examples/pyproject.toml 与 examples/pylintrc)。
  3. CI 门禁:用 --fail-under=9 之类的阈值替代“零告警”硬性要求;需要“评分达标但某类消息必须失败”时,可在后续版本使用 --fail-on 组合(2.9 引入,见 doc/whatsnew/2/2.9/summary.rst)。
  4. 命名规范:需要豁免自定义前缀时,优先用 good-names-rgxs;不要依赖关闭整个 invalid-name。
  5. 安全提示:升级后 python -m pylint 不再从当前目录导入用户模块,依赖该行为运行 lint 的脚本请改用 pylint 命令形式或显式传入文件路径。

六、参考资源

登录后查看全文
pylint