简介:Pychecker 0.8.19 是面向 Python 开发者的静态代码分析工具,专注于在程序运行之前发现潜在缺陷。它可检查变量类型是否匹配、是否存在未定义引用、未使用变量、函数与类是否过度庞杂,还能识别模块循环依赖、复杂表达式和异常处理漏洞,帮助开发者减少调试时间并提升代码可维护性。该工具包共 54 个文件,以 41 个 py 源码文件为主体,涵盖核心检查器、选项配置、输出模块及完整测试用例;同时附带 README、ChangeLog、setup.py、pycheckrc 等说明文档和安装配置内容,便于理解结构或直接部署。整个压缩包约 176KB,体量轻巧,适合本地快速试用。目前已有 391 人学习下载。安装后只需将 pychecker 脚本加入系统路径即可运行检查;与 Pylint、Flake8 等工具联合使用,能形成更完善的代码质量保障流程。对于希望了解静态检查实现原理或扩充自身工具链的 Python 开发者,这份资源提供了可直接阅读的源码示例和测试参考,具有实用价值。
1. pychecker 工具到底是什么:先回答三个最常被问的问题
pychecker 工具是 Python 静态代码检查里一个偏务实的存在:不讨论代码风格,也不做类型推导,只盯着运行时才会炸的错误——未定义的名字、调用不存在的方法、参数个数不匹配。我第一次认真用它是在一次跨平台系统重构,CI 加的检查把三个“看起来能跑”的历史模块全拦了下来,解释器确实会在特定分支上抛 NameError。人能看懂逻辑,机器一跑就翻车,这是它最典型的拦截场景。
这篇笔记只回答四件事:pychecker 查什么、为什么值得在 pylint 和 flake8 之外再养一个静态检查环节、怎么配置接进本地和 CI、以及真实项目里它最常出问题的五个地方。适合两类读者:项目刚做大、被运行时错误折磨的开发者;已经用别的 linter、但觉得风格检查覆盖不到运行前错误的团队。所有命令都标注了参数含义,配置给了可以直接改的版本。
先讲原理,因为搞清楚它查什么,才知道哪些坑是工具本身修不了的。
2. pychecker 的检查原理与选型边界:AST 为什么能提前抓 NameError
2.1 pychecker 在代码上做什么:AST 扫描、符号表与告警生成
pychecker 的工作方式和解释器跑代码完全不同。它把 .py 文件读进来后,先按编译器流程把源码解析成一棵抽象语法树(AST),然后在这棵树上做两件事:建符号表、查引用关系。
符号表就是一张“这个文件里有哪些名字”的账本。pychecker 遍历每个 def、class、import 和赋值语句,把定义过的名字记下来;再遍历所有使用名字的地方,比如函数调用、属性访问、变量引用,逐个对照符号表。查不到定义就报 undefined name;查到定义但使用方式不匹配,比如函数只接受两个参数却被传了三个,就报参数个数错误。整个过程不执行任何代码,所以叫静态检查。
这一步的意义在于,很多运行时错误是分支相关的。比如下面这段代码,如果 config 内容永远走第一个分支,谁都不会发现第二个分支里少了字母:
import json def parse_config(config_path): with open(config_path) as f: raw = f.read() if raw.startswith("{"): data = json.loads(raw) else: data = json.load(raw) # 少了 s,pychecker 会在这里告警 return data["items"]pychecker 扫到 else 分支时,会顺着 import 的引用关系发现 json 模块里没有 load 这个属性,于是直接给出告警,不用等测试用例覆盖到那个分支。这就是 AST 静态分析相对测试的最大优势:对分支天然全遍历,不存在“没测到就漏掉”。
当然,AST 分析有它的盲区。它看不到函数内部通过字符串拼出来的调用,比如 getattr(obj, method_name)() 这种写法,pychecker 只能确认 getattr 本身合法,查不到 method_name 指向的字符串是否存在。所以 pychecker 的定位是低成本兜底,不是全部保障,第四章会专门讲它的误报和漏报边界。
2.2 与 pylint、flake8、mypy 的边界:什么时候该用 pychecker
团队里最常问的问题是:已经用了 flake8,为什么还要 pychecker?这个问题得从工具侧重点来看。
flake8 是 pycodestyle 加 pyflakes 的组合,前者管 80 字符换行、缩进这类风格,后者管未使用导入和部分未定义变量。pylint 覆盖面最广,风格、命名、重复代码、坏味道都管,但默认配置在真实项目里误报率高,很多团队花在配置上的时间比修复还多。mypy 做类型检查,前提是代码里有类型标注,存量代码没有标注就基本扫不出东西。
pychecker 卡的位置很特殊:不管风格,只查运行时会炸的那几类错误。因此它的告警密度低,但每条告警含金量高。我一般给团队排这样的优先级:先跑 pychecker 修运行期错误,再跑 flake8 清理风格,最后用 mypy 做增量类型覆盖。顺序反过来,你会先被几百条风格告警淹没,真正致命的 NameError 反而沉在最底下没人看。
| 工具 | 检查对象 | 典型输出 | 适合场景 |
|---|---|---|---|
| pychecker | 未定义名、参数不匹配、未使用变量/导入 | NameError 类运行期错误 | 存量代码扫雷、CI 前置拦截 |
| flake8 | 风格 + 部分未定义名 | 样式问题、未使用导入 | 统一代码风格 |
| mypy | 类型标注一致性 | 类型不匹配 | 有类型标注的新代码 |
| pylint | 风格 + 复杂度 + 坏味道 | 大量可读性告警 | 代码评审辅助 |
实际项目里这不是单选题。某公司的支付网关项目就是三层全上:pychecker 配 pre-commit 拦提交前的低级错误,flake8 配 CI 管格式,mypy 只在新增模块上开严格模式。每层工具面对的数据量都不大,告警信噪比才高。
反过来,也有不适合用 pychecker 的场景:一次性脚本、Jupyter Notebook、原型验证代码。这些代码生命周期短,运行一次就扔,静态检查的投入产出比不划算。判断标准就一条:这段代码会不会被多人长期维护。不会,就别上工具。
2.3 最小跑通命令:安装、扫描与读输出的三步
先给最小可复现的命令。假设有个 demo.py,里面故意写了一个未定义变量:
pip install pychecker pychecker demo.py第一条命令把 pychecker 装进当前 Python 环境,第二条对 demo.py 做一次完整扫描。输出格式通常是 文件名:行号: 告警类型: 描述,比如 demo.py:12: Undefined variable: 'parse_config'。文件路径和行号用来定位,告警类型用来筛严重级别。常见的类型前缀有 E 和 W,E 级是运行时会炸的错误,W 级是潜在问题。
pychecker 默认会扫当前目录下所有 .py 文件,所以直接跑 pychecker 不带参数也行,但我建议永远显式传路径。原因有两个:一是不同版本对无参数行为有调整,二是显式传路径能避免扫进 venv 或 node_modules 这些不该碰的目录。一次扫多个文件用空格分隔,扫整个包直接传包名:
pychecker app/ tests/ demo.py这条命令让 pychecker 把三个路径下的 Python 文件都扫一遍。只想看告警不想被进程退出码打扰,加 --quiet;要重定向输出,加 --no-color 关掉颜色,避免报告文件里全是转义序列。
第一次在存量项目上跑 pychecker,我建议先别急着修,把输出存下来做一个基线:
pychecker app/ > first_scan.txt 2>&1 # 先存基线,方便后续对比 wc -l first_scan.txt先看告警总量,判断项目的中毒程度;再按告警类型统计,决定先修哪批。我踩过的坑是:把第一次全量清零当目标,结果在配置上花了两周,而不是在代码修复上花两天。
提示:第一次扫描先别开 fail-on 这类严格参数,否则你会被存量告警淹没。先摸底,再定阈值。
3. 把 pychecker 接进日常开发流:配置文件、pre-commit 与增量检查
3.1 配置文件:忽略规则、白名单与告警级别
pychecker 支持通过配置文件管理规则。我习惯在项目根目录放一个 setup.cfg,在文件末尾加 [pychecker] 段,这样和 flake8 的 [flake8] 段放一起,整个项目的 lint 配置在一个文件里就能看全。
配置文件最核心的是三类内容:忽略哪些告警、排除哪些路径、告警级别怎么设。忽略规则解决误报,排除路径解决生成代码和第三方代码,告警级别决定哪些告警升级成阻断 CI 的错误。
[pychecker] # 逗号分隔的告警类型黑名单 ignore = W0611, # 未使用的导入,交给 flake8 统一管 W0612, # 未使用的变量,存量代码太多,后续再开 # 排除目录,支持通配符 exclude = */migrations/* */generated/* build/* dist/* # 把 E 级告警当成错误,非零退出 fail_on = E # 最大扫描文件数,防止误扫大目录 max_files = 2000每个字段对应一种常见场景。W0611 和 W0612 这类未使用告警在存量代码里数量巨大,先关掉让团队聚焦在真正的运行期错误;migrations 和 generated 是框架自动生成的目录,扫了只会制造噪音;fail_on = E 表示只有 E 级告警才让命令非零退出,这样 CI 只被真正的错误阻断,警告可以慢慢消化。
配置文件和命令行参数的关系要明确:命令行参数优先级高于配置文件。比如配置文件里 fail_on = E,命令行里再传 --fail-on=W,实际生效的是命令行。这个特性在 CI 里很有用,可以让本地宽松、CI 严格,同一份代码在不同环境有不同阈值。
3.2 把 pychecker 接入 pre-commit 钩子:让检查发生在提交前
静态检查最理想的位置是提交前,因为这时候改动还在开发者本地,修复成本最低。pre-commit 是目前最通用的接入方式,它的配置文件 .pre-commit-config.yaml 里声明要跑的钩子,然后 pre-commit install 把它挂到 git 的 pre-commit 钩子上。
常见做法是给 pychecker 写一个本地钩子,因为 pychecker 不一定有官方维护的 pre-commit 镜像仓库。用本地钩子,pychecker 随开发者本地环境安装,不走网络拉取,可控性更好:
repos: - repo: local hooks: - id: pychecker name: pychecker entry: pychecker language: system files: \.py$ exclude: | ^(build/|dist/|.*/migrations/|.*/generated/) args: ["--fail-on=E"]这段配置的含义:pychecker 作为本地钩子运行,entry 指定命令,files 限定只检查 .py 文件,exclude 排除生成目录,args 传入只有 E 级告警才失败。language: system 是关键,它告诉 pre-commit 直接用当前环境的 pychecker,不为钩子单独建隔离环境。
注意:本地钩子依赖开发者本地已装 pychecker。README 里必须写清安装步骤,否则新同事第一次提交就会卡在钩子上。
3.3 pychecker 增量检查:只扫改动文件的两种做法
全量扫描在代码库超过一定规模后会很慢,而提交前检查其实只需要覆盖本次改动涉及的文件。增量检查要做两件事:找出改动文件,对它们执行 pychecker。
最简单可靠的做法是借助 git diff 拿文件列表:
git diff --name-only HEAD~1 -- '*.py' | xargs -r pychecker拆开看:git diff --name-only 输出最近一次提交里改动的文件路径,-- '*.py' 过滤出 Python 文件,xargs -r 把列表传给 pychecker。-r 参数表示列表为空时不执行,避免没改 Python 文件时 pychecker 反而去扫全目录。注意 -r 是 GNU xargs 的扩展,macOS 自带 xargs 不支持,在 macOS 上需要先用 wc -l 判断列表非空再传给 xargs。
第二种做法是在 CI 里做 PR 范围的增量检查,拿目标分支和当前分支做 diff,对差异文件逐个扫描。这里有个坑:git diff 默认输出顺序按路径排,如果同一个 PR 里既有文件改动又有文件删除,xargs 会把已删除的文件也传给 pychecker,导致“文件不存在”报错。解决是先过滤掉状态为 D 的路径:
git diff --name-status HEAD~1 -- '*.py' \ | awk '$1 != "D" {print $2}' \ | xargs -r pychecker # 只把存活文件交给 pychecker--name-status 的每一行开头是一个字母,M 表示修改,A 表示新增,D 表示删除。awk 过滤掉 D,只把存活文件的路径交给 pychecker。如果仓库是 monorepo,建议在改动所属的子包里执行增量检查,不要从仓库根目录跑,否则 pychecker 会一路扫到无关模块,耗时翻倍。
还有一个边角情况:仓库只有一个提交时 HEAD~1 不存在,git diff 会直接报错。这时改用 git diff --name-only --cached 针对暂存区做检查,语义和提交前检查完全一致。
4. pychecker 高频踩坑与排查:从误报到性能失控的 5 个真实问题
4.1 误报太多被团队禁用:先调阈值再谈推广
现象:接入 pychecker 的第一周,开发者本地提交被几十条告警频繁打断,其中相当一部分“看起来是对的代码”也被报,团队开始有人用 --no-verify 跳过检查。
原因:大多数团队第一次接入时把默认规则全量开启,而默认规则是为通用场景设计的,对业务代码里的动态特性缺乏感知。更重要的是存量代码本身带病,全量检查等于一次性把历史欠账全翻出来,开发者自然抗拒。
解决:分三步。第一步,先全量扫描,按告警类型统计数量,挑出最多的三类关进忽略列表。第二步,设置 fail_on = E,让低级别告警只作为提示,不阻断提交。第三步,给团队两周适应期,期间只修 E 级告警,W 级攒着每周集中处理一次。我见过最成功的接入,是把告警数从几千压到一两百之后,才开始要求新代码零告警。这个节奏的底层逻辑是:先让工具不烦人,再让工具变严格。跳过检查的开发者不是不重视质量,而是工具给的正反馈太少、负反馈太多。
4.2 动态导入被误判为 undefined:白名单与显式注册
现象:代码里用了 importlib.import_module("plugins.billing") 或 getattr(namespace, handler_name),pychecker 报 Undefined name 或 Undefined attribute,程序运行时却完全正常。
原因:pychecker 解析不了字符串参数的内容。import_module 的字符串参数在它眼里就是一个普通字符串,它不知道模块是否存在,也不知道 getattr 第二参数指向哪个属性。这是 AST 分析的天然盲区,不是 bug。
解决:看动态导入的数量。少数几个固定模块名,加白名单最省事。数量很大,说明设计上需要调整——常见做法是改成显式注册表,把所有可导入模块在常量字典里列出来,pychecker 能扫到字典的值,误报自然消失:
# modules.py 显式注册,替代动态拼路径 AVAILABLE_PLUGINS = { "billing": "plugins.billing", "invoice": "plugins.invoice", } def load_plugin(name): module_name = AVAILABLE_PLUGINS[name] return importlib.import_module(module_name)改完后 pychecker 依然不知道 import_module 字符串内容,但它能确认 module_name 来自一个已定义的字典,不再报 Undefined name。额外收益是插件列表变得可搜索、可审计,比散落在代码里的硬编码字符串好维护得多。
提示:误报是静态分析工具的固有边界,不是 bug。处理顺序是先确认代码确实依赖动态特性,再决定白名单还是改注册表。
4.3 pychecker 扫生成代码目录:排除规则的正确写法
现象:ORM 模型、protobuf 生成的 *_pb2.py、接口客户端代码等自动生成的文件,每次跑 pychecker 都报几十条告警,而且这些文件下次重新生成时告警又出现,修了也白修。
原因:生成代码的结构特征是大量使用动态属性和反射,AST 分析对这类代码误报率本来就高。更关键的是这些文件不该由人手工修改,pychecker 扫它们是纯粹的噪音。
解决:在配置文件的 exclude 里列全。注意通配符写法:/generated/能匹配任意层级下的 generated 目录,写成 generated/* 就只匹配根目录下的。另一个容易漏的是 pytest 的 .pytest_cache、tox 的 .tox,这些也要排除。我习惯按文件特征排除,比如_pb2.py、_grpc.py,这样生成目录改名字,规则依然有效:
[pychecker] exclude = .tox/* .pytest_cache/* *generated/* *_pb2.py *_grpc.py4.4 pychecker 与其它 linter 规则冲突:统一告警级别的顺序
现象:同一个改动,pychecker 报未使用的导入,flake8 也报未使用的导入,两边都拦,修了一边另一边还报,同一个问题被烦两次。
原因:pychecker 和 pyflakes 在未使用导入、部分未定义变量上确有重叠,两个工具各自按自己的告警编号输出,不会自动协商。
解决:在接入前把规则分工写清楚。我的建议是:运行期错误全留给 pychecker,风格和未使用类规则全交给 flake8。具体到配置,就是在 pychecker 的忽略列表里关掉与 pyflakes 重叠的 W 类告警,在 flake8 配置里保持 F401 类规则开启。两个工具的告警集变成互补,开发者看到的每条告警都有唯一归属。如果两个工具的配置各自为政,这个问题会在项目发布前集中爆发,最后总有人忍不住改配置绕过检查。团队进新人时把这份分工写进 README,能省大量答疑时间。
4.5 大仓库 pychecker 耗时失控:分批扫描与缓存
现象:代码库从几万行涨到几十万行后,pychecker 全量扫描从几秒涨到几分钟,CI 任务排队,开发者本地提交前检查动辄卡半分钟,有人开始跳过钩子。
原因:pychecker 单文件扫描本身不慢,但它是纯 Python 进程,每次启动都要加载解释器和编译 AST,对大量文件逐个启动进程的开销累积起来很大。而且它没有跨进程缓存,同样的文件每次扫描都要重新解析。
解决:治本是前面说的增量检查,只在改动文件上跑。如果全量扫描有硬需求,比如发布前的完整性检查,可以用 xargs 做并行分批:
git diff --name-only HEAD~1 -- '*.py' | xargs -P 4 -n 20 pychecker # 4 进程并行,每批 20 个文件-P 4 表示同时起 4 个 pychecker 进程,-n 20 表示每 20 个文件分一批。代价是输出顺序会乱,因为多进程并行时谁先结束谁先写。要稳定排序,把输出重定向到文件再 sort,或者把 -P 调到 2 降低乱序程度。性能经验值:单进程大约每秒扫 200 到 500 个小型 Python 文件,超过这个量考虑并行,低于这个量并行反而因进程启动开销更慢。
注意:配置文件里 max_files 到达上限后,pychecker 会静默跳过剩余文件。CI 日志里扫描文件数比预期少,先检查是不是被这个参数截断。
5. 让 pychecker 真正守住质量的三个验证技巧
接入之后,怎么验证 pychecker 真的在起作用,而不是变成一条形同虚设的 CI 步骤?我的做法是备一个体检文件,里面故意埋几类典型错误,每次升级 pychecker 或改配置后,拿它做回归验证:
# smoke_check.py —— 故意埋错的体检文件 def helper(a, b): return a + b def main(): result = helper(1) # 参数个数不匹配,应报错 print(caculate(result)) # 未定义变量,应报错 return result if __name__ == "__main__": main()跑 pychecker smoke_check.py,预期至少看到一条参数不匹配和一条未定义变量。如果升级后这两条消失,说明版本行为或配置有变化,需要重新校准。这个文件放在 tests/ 目录下的独立位置,不进业务包,它是一把校验尺。
第二个技巧是给告警量设阈值,不只看 exit code。把 CI 检查结果保存成报告文件,解析告警总数,超过阈值就失败。这样即使某次改动引入大量新告警但没触发 E 级失败,总量阈值也能拦住。实现上用 wc -l 加 awk 做数值比较,几行 shell 就能搭起来。
第三个技巧是养成改动即扫描的习惯,不要攒到提交前才跑。pychecker 的价值在反馈闭环的及时性,越早让告警出现在终端里,修复成本越低。我自己习惯改完一个函数立刻跑一次单文件扫描,确认干净再继续下一个。这个习惯帮我避过至少两次生产事故,都是测试没覆盖到、pychecker 提前喊了一声的场景。
最后说一个踩坑换来的习惯:不要把告警编号背下来,而是把配置文件当活文档,每关一条规则就写一行注释说明为什么。两年前我在一个遗留项目上看到有人把忽略列表写到四十多条,每条都没注释,谁也说不清这些规则是在防误报还是在掩盖债务。从那天起,我的所有 lint 配置都强制带注释,规则可以关,但理由必须留下。希望帮到你。
本文还有配套的精品资源,点击获取