Pylint 3.0 大版本发布全解析:新特性、破坏性变更与升级迁移指南
Pylint 3.0 大版本发布全解析:新特性、破坏性变更与升级迁移指南
Pylint 3.0 是 Pylint 历史上重要的主版本里程碑(2023-10-02 发布 3.0.0),它正式支持 Python 3.12,带来了全新的 json2 报告器、pyreverse 图表工具的显著增强,并集中执行了一批酝酿已久的破坏性变更与弃用清理。本文以仓库 doc/whatsnew/3/3.0/index.rst 为主线,结合 pylint 源码中的真实实现,系统梳理 3.0 系列(3.0.0 → 3.0.4)的全部新特性、行为变化、新增检查项与迁移要点,帮助你完成从 2.x 到 3.x 的平滑升级。
版本总览与发布节奏
Pylint 3.0 系列在 3.0.0 发布后迅速跟进了一系列补丁版本,主要围绕 Python 3.12 兼容性与误报修复:
| 版本 | 发布日期 | 主题 |
|---|---|---|
| 3.0.0 | 2023-10-02 | 主版本:Python 3.12 支持、json2 报告器、破坏性变更 |
| 3.0.1 | 2023-10-05 | 修复 inherit-non-class 误报、enum + dataclass 装饰器崩溃 |
| 3.0.2 | 2023-10-22 | PEP 695 泛型语法误报、消息转义、inconsistent-quotes 等 |
| 3.0.3 | 2023-12-11 | unnecessary-lambda、浅拷贝建议修正、目录递归遍历问题 |
| 3.0.4 | 2024-02-23 | used-before-assignment、no-member、nested-min-max 等误报修复 |
版本 3.0.0 的几个关键全局事实(源码与发布说明共同确认):
- Python 3.12 正式支持:运行环境要求 Python 3.8+,但被检查的代码可以基于更早的 Python 版本(通过
--py-version指定)。 - astroid 依赖升级到 3.0.0:性能与推断能力随之提升。
- 代码完全类型化:
Pylint now exposes its type annotations,仓库根目录也新增了 py.typed 标记文件。 - 依赖精简:移除了
wrapt、setuptools等依赖。 invalid-name默认不再强制名称最短 3 个字符(详见下文"命名检查"一节)。
核心亮点一:全新的 json2 报告器
3.0 最重要的新功能之一,是新增了 json2 报告器。它相比旧的 json 报告器提供了更丰富、更易解析的结构化输出。在 pylint/lint/base_options.py 的 output-format 帮助文本中,json2 被明确标注为 "improved json format",官方建议优先使用它,旧的 json 报告器将不再维护。
使用方法
pylint my_module.py --output-format=json2
# 或使用短格式
pylint -f json2 my_module.py
输出结构示例
官方发布说明给出了如下示例输出:
{
"messages": [
{
"type": "convention",
"symbol": "line-too-long",
"message": "Line too long (1/2)",
"messageId": "C0301",
"confidence": "HIGH",
"module": "0123",
"obj": "",
"line": 1,
"column": 0,
"endLine": 1,
"endColumn": 4,
"path": "0123",
"absolutePath": "0123"
}
],
"statistics": {
"messageTypeCount": {
"fatal": 0,
"error": 0,
"warning": 0,
"refactor": 0,
"convention": 1,
"info": 0
},
"modulesLinted": 1,
"score": 5.0
}
}
与旧 json 报告器的差异
对照 pylint/reporters/json_reporter.py 中 JSONReporter 与 JSON2Reporter 两个类的实现,差异非常清晰:
- 顶层结构:
json直接输出一个扁平的消息数组;json2将消息收纳到"messages"键下,并新增"statistics"统计块。 - 字段命名:
message-id(kebab-case)在json2中改为遵循 camelCase 约定的messageId(见 JSONMessage 的 TypedDict 定义)。 - 新增字段:每条消息增加
confidence(置信度,如HIGH、INFERENCE,取自CONFIDENCE_MAP)与absolutePath(绝对路径);旧json的path字段被保留并新增了绝对路径版本。 - 统计信息:
json2的serialize_stats()(pylint/reporters/json_reporter.py#L168-L196)输出:messageTypeCount:fatal / error / warning / refactor / convention / info 六类消息计数;modulesLinted:被检查的模块数量(len(stats.by_module));score:依据evaluation配置项计算出的评分。
其中 score 的算法在源码中直接调用 evaluation 表达式(默认表达式见 pylint/lint/base_options.py 的 evaluation 选项定义),可用变量包括 fatal、error、warning、refactor、convention、info 以及 statement。
# 也可以将 json2 输出直接写入文件
pylint -f json2 my_module.py > pylint_report.json
核心亮点二:pyreverse 图表的全面增强
3.0 对 pyreverse(Pylint 内置的 UML 图生成工具)进行了多项重要改进,相关选项全部定义在 pylint/pyreverse/main.py。
新的默认配色(对色盲更友好)
pyreverse 采用了一套新的默认调色板,取自 Paul Tol 的色觉友好配色方案,定义在 pylint/pyreverse/main.py#L35-L46 的 DEFAULT_COLOR_PALETTE:
DEFAULT_COLOR_PALETTE = (
"#77AADD", # light blue
"#99DDFF", # light cyan
"#44BB99", # mint
"#BBCC33", # pear
"#AAAA00", # olive
"#EEDD88", # light yellow
"#EE8866", # orange
"#FFAABB", # pink
"#DDDDDD", # pale grey
)
如果你喜欢其他颜色,可以通过新增的 --color-palette 选项指定自定义颜色列表:
pyreverse --color-palette="#112233,#445566,#778899" my_package
该选项类型为 csv,与 --max-color-depth(按包深度着色)配合使用。
新选项 --show-stdlib / -L
标准库模块默认不再出现在图中(与 --show-builtin 的行为类似),需要通过新选项显式包含:
pyreverse -L my_package # 包含标准库模块
pyreverse --show-stdlib my_package
新选项 --no-standalone
用于从图中排除没有关联关系的孤立节点,只显示有连接关系的节点,让大项目图更聚焦:
pyreverse --no-standalone my_package
源码中该选项的帮助文本为 "Only show nodes with connections."(pylint/pyreverse/main.py#L195-L202)。
包依赖图中标注仅用于类型检查的导入
在 pyreverse 的包依赖图中,当某个模块只是出于类型检查目的(如 TYPE_CHECKING 块内)导入另一个模块时,会以特殊方式标注,便于区分运行时依赖与类型依赖。
输出统计信息与 --verbose
pyreverse 现在会在运行时打印包统计信息;新增的 --verbose 标志用于输出包含已解析模块的原始日志(类似旧版本输出),调试时使用:
pyreverse --verbose my_package
移除的内容
.vcg输出格式(Visualization of Compiler Graphs)被移除,直接支持的格式收缩为dot、puml/plantuml、mmd、html(见 pylint/pyreverse/main.py#L27-L33 的DIRECTLY_SUPPORTED_FORMATS);其他格式会尝试调用 Graphviz 的dot工具生成。__implements__机制被完全移除:它基于 2001 年提出、2006 年被否决的 PEP 245,pyreverse 也不再考虑__implements__生成图。
破坏性变更:必须关注的迁移点
1. 消息启停的顺序语义变化
在 3.0 中,对单个消息的 --enable / --disable 即使在同一次配置中后面跟着 --enable=all 或 --disable=all,也会生效。例如:
pylint my_module --enable=fixme --disable=all
以前 --disable=all 会覆盖前面的 --enable=fixme,现在 fixme 消息会被正常发出。若要恢复旧行为,删除多余的靠前选项即可。相关启停逻辑在 pylint/lint/message_state_handler.py 中实现,其中 _msgs_state 维护每个消息的启用状态,disable 对 "all" 的处理会保留 Pylint 自身必须开启的消息(见 pylint/lint/message_state_handler.py#L91-L97)。
2. Python 3.7 运行支持移除
不再支持用 Python 3.7 启动 Pylint(即运行环境至少为 Python 3.8)。但被检查的代码如果基于 3.7 编写,仍可通过 --py-version=3.7 指定目标版本继续检查:
pylint --py-version=3.7 legacy_code.py
3. try 块中的禁用注释现在作用于 except 块
try:
# pylint: disable=some-message
risky()
except Exception:
risky_again() # 3.0 起这里的 some-message 也会被禁用
此前只有存在 else 子句时才会生效,3.0 统一了这一行为。
4. pylint.config.PYLINTRC 移除
经过弃用期后,pylint.config.PYLINTRC 属性被删除,请改用 pylint.config.find_default_config_files 生成器。这一用法在 pylint/lint/run.py#L166 中可以看到实际应用:
default_file = next(config.find_default_config_files(), None)
5. MASTER / master 配置节不再允许
在 setup.cfg 或 tox.ini 中,配置节必须以下划线约定的工具名前缀开头。旧的 [MASTER] / [master] 写法不再合法,请改为:
[pylint.main]
max-line-length=100
6. overgeneral-exceptions 只接受全限定名
overgeneral-exceptions 选项现在只识别全限定名称,例如 builtins.Exception 而不是 Exception。如果你覆盖过该选项,必须改用全限定名;3.0 中仍会发出警告,该警告将在 3.1.0 移除。
7. epylint 从仓库移除
epylint(Emacs 相关工具)已迁移到独立项目维护,不再随 Pylint 分发。
8. 配置解析彻底切换到 argparse
所有与 optparse 相关的配置解析代码被删除,配置加载统一由 pylint/config/config_file_parser.py 完成,支持 .pylintrc、pyproject.toml(TOML)、setup.cfg / tox.ini(INI)等多种格式。
需要用户操作的变更(一):invalid-name 与命名长度检查
invalid-name 消息此前默认会检查名称是否至少 3 个字符——这实际上把"大小写风格"和"名称长度"两件不相干的事混在了一起,用户经常对此感到意外。3.0 移除了这个默认长度限制。
如果你仍希望保留最短长度检查,官方给出两种恢复方式:
方式一:接受 disallowed-name 消息,配置正则匹配 1~2 个字符的名称(需同时启用 disallowed-name):
[pylint.main]
bad-names-rgxs="^..?$"
enable=disallowed-name
方式二:希望继续使用 invalid-name 消息,则通过细粒度的正则选项(如 variable-rgx、class-rgx、function-rgx 等)自行控制。旧版正则可在移除长度要求的 PR 中查找。
从源码看,pylint/checkers/base/name_checker/checker.py#L279-L287 中 bad-names-rgxs 的默认值已为空字符串 "",且 disallowed-name 与 invalid-name 是两个独立消息(见 checker.py#L215)。同时 3.0 放宽了 typealias 名称的整数限制(Good2Name、GoodName2 合法,_1BadName 仍非法),且 Union 不再被视为命名检查意义上的类型别名。
需要用户操作的变更(二):隐式布尔检查合并
pylint.extensions.emptystring(比较空字符串)与 pylint.extensions.compare-to-zero(比较零)两个扩展被移除,其检查能力并入隐式布尔检查器(ImplicitBooleanessChecker):
compare-to-zero更名为use-implicit-booleaness-not-comparison-to-zero;compare-to-empty-string更名为use-implicit-booleaness-not-comparison-to-string;- 两个新消息默认关闭,需要显式
--enable; - 旧扩展必须从
load-plugins选项中移除,否则会报错。
从 pylint/checkers/refactoring/implicit_booleaness_checker.py 的源码可以确认:消息 C1804(字符串版本)与 C1805 级别的零比较检查均设置了 "default_enabled": False(见 implicit_booleaness_checker.py#L81-L90),且旧消息名如 len-as-condition 保留了 old_names 映射。这些检查共享大部分触发条件,因此也更易维护、性能更好,同时消息文本变得更明确、更可操作。
迁移示例:
# 旧写法(3.0 中失效)
load-plugins=pylint.extensions.emptystring,pylint.extensions.compare-to-zero
# 新写法
enable=use-implicit-booleaness-not-comparison-to-zero,use-implicit-booleaness-not-comparison-to-string
新增检查项(New Checks)
3.0 新增了三个内置检查和一个扩展检查,可在 doc/data/messages 目录下找到各自的文档化说明:
invalid-field-call(E3701)
新增独立的 DataclassChecker 模块(pylint/checkers/dataclass_checker.py),检查 dataclasses.field() 的非法用法——它只能作为 dataclass 类体内的赋值值,或出现在 make_dataclass() 函数中。源码中的检测逻辑通过 safe_infer 确认 field 确实来自 dataclasses 模块(_is_dataclasses_module),再检查其作用域是否为 dataclass 类或 make_dataclass 调用。
from dataclasses import dataclass, field
@dataclass
class Good:
x: int = field(default=0) # 合法
y = field(default=0) # E3701: 非法使用 field()
return-in-finally
在 finally 子句中检测到 return 语句时发出警告(pylint/checkers/base/basic_checker.py#L264),因为 finally 中的 return 会覆盖 try 块的返回值,通常是隐藏 bug。
kwarg-superseded-by-positional-arg
当函数同时具有位置限定参数(positional-only)和关键字可变参数字典(**kwargs),调用时用关键字实参传入与位置限定参数同名的参数时发出警告——该关键字会被并入 **kwargs,行为可能与直觉相悖(pylint/checkers/typecheck.py#L416)。
扩展:prefer-typing-namedtuple(R6105)
CodeStyleChecker 新增消息,建议在 Python 3.6+ 上将 collections.namedtuple 调用改写为继承 typing.NamedTuple 的类(pylint/extensions/code_style.py#L67-L77)。需要加载扩展并显式启用:
load-plugins=pylint.extensions.code_style
enable=prefer-typing-namedtuple
# 不推荐
Point = collections.namedtuple("Point", ["x", "y"])
# 推荐
class Point(typing.NamedTuple):
x: int
y: int
误报修复(False Positives Fixed)精选
3.0.0 共修复了 24 处误报,3.0.1~3.0.4 又追加修复 12 处,按主题归纳如下:
变量作用域与赋值相关
used-before-assignment:循环中使用名称且依赖except块中早期赋值 +else: continue时不再误报(#6804);finally块中try与各异常处理都有赋值时不再误报(#9451);用法与赋值被同一条件在不同语句中保护时不再误报(#8167);TYPE_CHECKING导入用于多个作用域时修复漏报(#8198)。unused-argument:__new__未使用__init__的全部参数时不再误报(#3670)。undefined-variable/unused-argument:Python 3.12 泛型类型语法(PEP 695)下不再误报(#9193)。
类型与注解相关
no-member:装饰器提供的函数属性不再误报(#9246)。inherit-non-class:泛型 Protocol 不再误报(#9106)。isinstance-second-argument-not-valid-type:联合类型包含None时不再误报(#8424)。pointless-string-statement:Python 3.12 类型别名上的 docstring 不再误报(#9268)。invalid-name:enum.Enum中无赋值的类型注解类变量不再误报(#7402);typealias名称允许数字(#8485)。
表达式与调用相关
unnecessary-lambda:调用带关键字参数而 lambda 不带时不再误报(#9148);lambda 在函数体中使用自身参数(非最终参数)时不再误报,如lambda foo: (bar if foo else baz)(foo)(#8496)。unnecessary-comprehension:浅拷贝建议修正——b = [x for x in a]不应建议改写为b = a(会改变引用语义),正确建议是list(a)(#9172)。nested-min-max:嵌套列表不再误报(#9307)。invalid-exception-operation:拼接异常类型元组不再误报(#9288)。logging-not-lazy:显式拼接的字符串参数不再误报(#8410)。unused-import:six.with_metaclass的用法不再误报(#7506);并开始遵守dummy-variables-rgx选项(#8500)。arguments-differ:重写__init_subclass__不再误报(#8919);positional-only-arguments-expected(#8555)与keyword-arg-before-vararg(#8570)的位置参数默认值场景均修复。no-value-for-parameter:类体内调用 staticmethod 不再误报(#9036)。
统计与循环相关
too-many-try-statements、too-complex、too-many-branches:正确统计try下的语句数(#7767)。unbalanced-dict-unpacking:改为检查每个待解包值的长度与解包目标数是否匹配,而非循环迭代值数量(#8156)。
框架适配
- 将
asyncSetUp加入默认的defining-attr-methods列表,消除unittest.IsolatedAsyncioTestCase下的attribute-defined-outside-init警告(#8403)。
漏报修复(False Negatives Fixed)
- 模块级函数在定义前被调用时,现在会发出
used-before-assignment(#1144)。 infer_kwarg_from_call()应用于更多检查,主要解决各类漏报(#7761)。- 函数内定义的
TypeAlias变量现在会被invalid-name检查(#8536)。 - 同时包含位置限定参数
name与**kwargs的函数,以关键字实参传入name时补发no-value-for-parameter(#8559)。 too-many-arguments现在计入 positional-only 与 keyword-only 参数(#8667)。- 内置方法如
dict.update()的调用现在发出assignment-from-no-return;list.sort()为保持一致改为发出assignment-from-no-return而非assignment-from-none(#8714、#8810)。 consider-using-augmented-assign扩展到字典和列表(#8959)。
其他 Bug 修复要点
并行与插件(--jobs)
duplicate-code(pylint/checkers/symilar.py#L726)与cyclic-import(pylint/checkers/imports.py#L245-L246)现在都支持并行检查模式;--jobs与--load-plugins也可同时使用(该能力曾在 astroid 2.5.0 中回归损坏)。并行实现位于 pylint/lint/parallel.py。
配置与推断
- 解析配置中的正则列表时,忽略花括号内的逗号(正则量词,如
{2,3}),避免误分割(#7229)。 ignored-modules现在对no-name-in-module生效(#7578)。sys.argv始终视为无法推断的值,不再使用传给 Pylint 的实际参数(#7710)。preferred-modules修复部分匹配回归,改为完整匹配(#8453)。- 修复
tomlkitv0.11.8 导致生成配置文件时的崩溃:Can't add a table to a dotted key(#8632)。
pyreverse 图表质量
- 类字段、箭头、类型注解不再重复显示(#8189、#8522、#8888)。
- 输出文件名含
/时不再崩溃(#8504)。 - dot 输出的换行错误修复(#8671)。
- 不再将
Optional添加进含None的|注解(#9014)。 - 多个类关联箭头只显示最强的一个(#9045)。
其他
--clear-cache-post-run现在也清理持有 AST 节点引用的 LRU 缓存(#8361)。- 未导入就使用
TYPE_CHECKING时不再崩溃(#8434);TYPE_CHECKINGelse-if 分支下的导入不再触发used-before-assignment误报(#8437)。 method-hidden在使用cached_property装饰器时不再误报(#8753)。- 修复
BaseChecker哈希不一致导致部分报告未导出的问题(#9001)。 - 修复 linter 自身相关消息(如
unknown-option-value)导致的统计数据丢失,从而影响 score、fail-on 等(#9059)。 - 修复 for 循环中一元操作数导致的 refactoring checker 崩溃(#9074)。
consider-using-generator对带default关键字的min()输出更准确(#8563)。- docparams 扩展把类型注释视为类型文档(#6287)。
内部变更与 API 清理(面向插件/二次开发)
3.0 是一次彻底的 API 收口,对编写自定义检查器或报告器的开发者影响重大:
get_message_definition从 base checker API 移除,请通过MessageStore访问消息定义。pylint.interfaces中继承Interface的类全部移除;Checker只应继承BaseChecker等pylint.checkers中的类型,Reporter只应继承BaseReporter。FileState现在必须传入modname与msg_store;collect_block_lines移除;Pylinter.current_name不再允许为 null。Reporter.set_output移除,改用reporter.out = stream。- 一批旧工具函数移除,官方给出替代方案:
MapReduceMixin→ 自行实现get_map_data/reduce_map_data;is_inside_lambda→utils.get_node_first_ancestor_of_type(x, nodes.Lambda);check_messages→utils.only_required_for_messages;is_class_subscriptable_pep585_with_postponed_evaluation_enabled→is_postponed_evaluation_enabled+is_node_in_type_annotation_context;get_python_path→discover_package_path(Python 3.3 起不再保证存在__init__.py,尤其 PEP 420 场景);fix_import_path→augmented_sys_path;get_global_option→checker.linter.config。
colorize_ansi只接受MessageStyle对象;ColorizedTextReporter只接受ColorMappingDict。Pylinter.check只接受字符串序列,不再接受单个字符串;check_single_file改为check_single_file_item。Run类(及 testutils 的_Run)的do_exit参数移除。MessageDefinition.may_be_emitted的py_version参数变为必填,或用MessageDefinition.is_message_enabled替代。Message的location参数必须是MessageLocationTuple;OutputLine参数数量必须精确(功能测试输出可用工具自动重新生成)。pylint.utils中的is_typing_guard、is_node_in_typing_guarded_import_block、is_node_in_guarded_import_block移除,改用is_sys_guard与in_type_checking_block组合。
其他变更
- 类型注解公开:Pylint 现在暴露自身的类型注解(py.typed),方便类型检查器使用。
pyproject.toml递归查找:配置查找会沿父目录一直向上到项目根或文件系统根。- 消息重命名:
unneeded-not更名为unnecessary-negation(旧名作为别名保留,见 pylint/checkers/refactoring/not_checker.py#L22-L25 的old_names)。 - 仓库根目录新增 CITATION.cff 引用元数据。
性能改进
- 得益于 astroid 对推断缓存的更好利用,Pylint 运行速度提升(至少约 5%)。
- 优化
is_trailing_comma(),并缓存class_is_abstract()。 - 当所有消息都被禁用时立即退出,避免无谓的完整扫描。
升级迁移清单
从 2.x 升级到 3.x 时,建议按以下清单逐项核对:
- 运行环境:确保 Python ≥ 3.8;被检查代码若基于 3.7 使用
--py-version=3.7。 - 消息启停:检查配置中
enable=...与disable=all的先后顺序是否符合新语义。 load-plugins:移除pylint.extensions.emptystring与pylint.extensions.compare-to-zero;如需对应检查,显式enable新消息名。invalid-name:如需保留最短 3 字符规则,配置bad-names-rgxs="^..?$"并启用disallowed-name,或自定义各命名正则。- 配置文件节:
setup.cfg/tox.ini中将[MASTER]改为[pylint.main]。 overgeneral-exceptions:改为全限定名(如builtins.Exception)。- 自定义代码:对照上文"内部变更"列表替换已移除的 API。
- 输出解析:若依赖 JSON 输出,建议切换为
json2并适配messages/messageId/statistics结构。
Pylint 3.0 在维持检查能力的同时,完成了运行环境、依赖、输出格式与内部 API 的现代化。对于普通用户,json2 与 pyreverse 增强是立即可用的收益;对于插件开发者,则需要按上述内部变更清单完成适配。更多细节可继续阅读仓库中的 完整变更记录说明 与各消息的文档化条目(doc/data/messages)。