Linter 与代码检查工具
Linter(代码检查器)负责在运行前发现风格违规、潜在缺陷与代码异味;类型检查器(Type Checker)则在静态层面验证类型注解的一致性。Python 生态中,Ruff、Flake8、Pylint、mypy 是四款最具代表性的工具。本节将对比它们的定位、给出可复用的配置,并演示如何接入编辑器与 CI。
前置知识
阅读本节前,建议先了解:VS Code 配置 Python 开发,其中已介绍如何在编辑器中启用 Ruff。
基础概念
需要先厘清几个容易混淆的概念:
| 术语 | 英文 | 职责 |
|---|---|---|
| Linter | Linter | 静态检查风格与潜在错误,如未使用变量、导入顺序 |
| 格式化工具 | Formatter | 只调整代码排版,不改语义,如 Black、Ruff format |
| 类型检查器 | Type Checker | 依据类型注解推断并检查类型一致性,如 mypy、Pyright |
| 代码异味 | Code Smell | 不影响运行但暗示设计问题的写法 |
| 规则代码 | Rule Code | 每条检查规则的短标识,如 F401、E501 |
| 内联忽略 | Inline Ignore | 用 # noqa 在当前行抑制告警 |
Linter 与 Formatter 不重复
Linter 会「指出问题」,Formatter 会「自动排版」。二者存在少量重叠(如行宽),实践中常见的分工是:Ruff 负责检查 + 修复 + 格式化,mypy 负责类型检查。
语法与用法
安装
# 一次性安装,推荐放入开发依赖
pip install ruff mypy flake8 pylint
# 或使用 uv 安装为开发依赖
uv add --dev ruff mypy# 验证安装
ruff --version
mypy --version快速上手
# Ruff 检查当前目录
ruff check .
# 自动修复可修复的问题
ruff check --fix .
# 格式化
ruff format .
# mypy 类型检查
mypy src/
# Flake8
flake8 .
# Pylint
pylint src/详细说明
四款工具对比
| 维度 | Ruff | Flake8 | Pylint | mypy |
|---|---|---|---|---|
| 实现语言 | Rust | Python | Python | Python |
| 检查类型 | 风格 + 缺陷 + 导入 + 部分安全 | 风格 + 缺陷(插件扩展) | 风格 + 设计 + 复杂度 | 类型为主 |
| 速度 | 极快(比 Flake8 快 10~100 倍) | 较慢 | 慢 | 中等 |
| 自动修复 | 支持大量规则 | 不支持 | 部分 | 不支持 |
| 可扩展性 | 内置规则丰富,插件生态较弱 | 插件生态成熟 | 插件与自定义检查器 | 插件(mypy.ini) |
| 配置位置 | pyproject.toml / ruff.toml | setup.cfg / .flake8 / tox.ini | pyproject.toml / .pylintrc | pyproject.toml / mypy.ini |
| 推荐度 | 新项目首选 | 存量项目维护 | 需要深度设计检查时 | 类型完备性要求高时 |
选择建议:
- 新项目:Ruff(检查 + 格式化)+ mypy(类型)。
- 存量项目:若已有大量 Flake8 配置与插件,可保留;逐步迁移到 Ruff 时可用
ruff check --select ALL辅助发现差异。 - 强规范团队:Pylint 的设计检查(如函数复杂度、类设计)可作为补充,但需大量调参。
Ruff 配置详解
pyproject.toml:
[tool.ruff]
# 目标 Python 版本,影响可用语法与规则集
target-version = "py312"
line-length = 88
# 排除目录
extend-exclude = ["build", "dist", "*.egg-info"]
[tool.ruff.lint]
# 规则集选择
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort 导入排序
"N", # pep8-naming 命名规范
"UP", # pyupgrade 语法升级
"B", # flake8-bugbear 常见缺陷
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"RET", # flake8-return
"ARG", # flake8-unused-arguments
"PTH", # flake8-use-pathlib
"RUF", # Ruff 专有规则
]
# 忽略规则
ignore = [
"E501", # 行宽由 formatter 处理,避免与 format 冲突
"B008", # 允许函数默认参数调用(如 FastAPI 依赖注入)
]
# 按文件类型放宽
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["ARG001"] # 测试夹具参数常未使用
"__init__.py" = ["F401"] # 允许未使用导入(用于对外导出)
[tool.ruff.lint.isort]
known-first-party = ["src"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "lf"常用规则代码含义:
| 规则代码 | 含义 |
|---|---|
F401 | 导入了但未使用 |
F841 | 赋值给变量但未使用 |
E722 | 使用裸 except: |
B006 | 可变默认参数(经典陷阱) |
B008 | 函数调用作为默认参数 |
N802 | 函数名未使用小写 |
UP006 | 可用 list 替代 List |
SIM108 | 可用三元表达式简化 if-else |
I001 | 导入未排序 |
mypy 配置详解
pyproject.toml:
[tool.mypy]
python_version = "3.12"
# 检查未注解的函数体
check_untyped_defs = true
# 禁止未注解的 def
disallow_untyped_defs = true
# 不完整的类型也报错
disallow_incomplete_defs = true
# 忽略缺失的第三方类型存根
ignore_missing_imports = true
# 对 Optional 严格检查
no_implicit_optional = true
# 未使用的 ignore 注释报错,避免失效
warn_unused_ignores = true
# 冗余类型转换报错
warn_redundant_casts = true
# 严格可选:开启后等价于开启大量严格开关
# strict = true
# 特定模块的覆盖
[[tool.mypy.overrides]]
module = ["pandas.*", "matplotlib.*"]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false带类型注解的示例:
"""演示 mypy 能捕获的典型问题"""
from typing import Optional
def greet(name: str, times: int = 1) -> str:
"""重复问候指定次数"""
return " ".join([f"Hello, {name}"] * times)
def find_user(user_id: int) -> Optional[dict[str, str]]:
"""按 ID 查找用户,可能返回 None"""
if user_id <= 0:
return None
return {"id": str(user_id), "name": "Alice"}
# mypy 会在此报错:Argument "times" has incompatible type "str"
# 错误!不要把字符串传给 int 参数
greet("Bob", "3")
# mypy 会报错:Optional 未做 None 判断
user = find_user(1)
print(user["name"]) # 错误!user 可能为 None
# 正确写法
if user is not None:
print(user["name"])运行结果:
$ mypy demo.py
demo.py:20: error: Argument "times" to "greet" has incompatible type "str"; expected "int" [arg-type]
demo.py:24: error: Value of type "Optional[Dict[str, str]]" is not indexable [index]
Found 2 errors in 1 file (checked 1 source file)Flake8 与 Pylint 配置
Flake8(.flake8 或 setup.cfg):
[flake8]
max-line-length = 88
extend-ignore = E203,W503
exclude = .git,__pycache__,.venv,build,dist
per-file-ignores =
__init__.py:F401
tests/*:E501Pylint(pyproject.toml):
[tool.pylint.main]
py-version = "3.12"
disable = [
"missing-module-docstring",
"too-few-public-methods",
"line-too-long",
]
ignore = [".venv", "build", "dist"]
[tool.pylint.format]
max-line-length = 88
[tool.pylint.design]
max-args = 8
max-locals = 20接入编辑器
VS Code(settings.json):
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
"python.analysis.typeCheckingMode": "basic"
}PyCharm:Settings → Tools → File Watchers 添加 Ruff,或在 Settings → Plugins 安装 Ruff 插件;mypy 可在 Settings → Editor → Inspections → Python → Type checker 中启用。
内联抑制告警
"""when-to-ignore 示例"""
import json # noqa: F401 # 临时保留,后续会用到
value: int = 42
# 忽略整行
result = json.loads('{"a": 1}') # noqa: E501
# 忽略指定规则,多个规则用逗号分隔
data = {"key": "value"} # noqa: E501, F841
# mypy 专用忽略
value2: int = "text" # type: ignore[assignment]
# 块级忽略(Ruff)
# ruff: noqa: E402
import os慎用忽略注释
忽略注释应写明原因,否则会掩盖真实问题。开启 warn_unused_ignores = true 后,失效的忽略会被自动发现。
实战示例
案例一:为新项目搭建检查流水线
项目结构:
myapp/
├── src/myapp/
│ ├── __init__.py
│ └── core.py
├── tests/
│ └── test_core.py
├── pyproject.toml
└── .pre-commit-config.yamlpyproject.toml(Ruff + mypy,定位为可复用的最小配置):
[tool.ruff]
target-version = "py312"
line-length = 88
[tool.ruff.lint]
select = ["E", "W", "F", "I", "B", "UP", "SIM", "ARG"]
ignore = ["E501"]
[tool.mypy]
python_version = "3.12"
check_untyped_defs = true
disallow_untyped_defs = true
warn_unused_ignores = truesrc/myapp/core.py:
"""核心业务逻辑"""
from dataclasses import dataclass
@dataclass
class Order:
"""订单数据模型"""
order_id: str
amount: float
quantity: int = 1
def total(self) -> float:
"""计算订单总金额"""
return self.amount * self.quantity
def summarize(orders: list[Order]) -> float:
"""汇总所有订单金额"""
return sum(order.total() for order in orders)本地执行:
ruff check . && ruff format --check . && mypy src/案例二:接入 pre-commit,在提交前自动检查
.pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2
hooks:
- id: mypy
additional_dependencies: [types-requests]安装与运行:
pip install pre-commit
pre-commit install # 安装 git hook
pre-commit run --all-files # 手动全量运行此后每次 git commit 都会自动检查并修复,不合规的提交会被阻断。
案例三:GitHub Actions 中做 CI 质量闸门
.github/workflows/lint.yml:
name: Lint & Type Check
on:
push:
branches: [main]
pull_request:
jobs:
quality:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: 安装 uv
uses: astral-sh/setup-uv@v3
- name: 安装依赖
run: uv sync --dev
- name: Ruff 检查
run: uv run ruff check .
- name: Ruff 格式检查
run: uv run ruff format --check .
- name: mypy 类型检查
run: uv run mypy src/ --python-version ${{ matrix.python-version }}格式检查用 --check 而非直接格式化
CI 中不应修改代码,只做校验:ruff format --check . 在格式不符时返回非零退出码,从而失败构建。
注意事项
ruff format与 Black 不要同时启用:两者格式化结果接近但不完全一致,会导致文件反复变动。- Ruff 与 Flake8 规则重叠:迁移期若同时运行,同一问题会报两次,建议逐步下线 Flake8。
- 不要盲目开启
select = ["ALL"]:会引入大量与业务无关的规则,建议按需逐步添加规则集。 - 忽略
E501的合理性:若已使用 formatter 控制行宽,再让 Linter 报行宽属于重复告警。 - mypy 的
ignore_missing_imports是双刃剑:会让无类型存根的库静默通过,掩盖类型丢失风险。 - 别把 lint 结果当成绝对真理:
# noqa与忽略配置应经过评审。
常见问题排查表:
| 现象 | 原因 | 解决 |
|---|---|---|
| Ruff 报错但 VS Code 无提示 | 扩展未启用或未选解释器 | 确认 Ruff 扩展已安装 |
ruff check --fix 反复报同一处 | 该规则不可自动修复 | 手动修改或配置忽略 |
| mypy 找不到第三方类型 | 缺少类型存根 | 安装 types-xxx 或配置 override |
| pre-commit 与本地结果不一致 | 版本不同步 | 统一 rev 与本地版本 |
| CI 因中文报错 | 编码问题 | 文件统一 UTF-8,配置 PYTHONUTF8=1 |
最佳实践
- 新项目默认选择 Ruff + mypy:一套工具覆盖检查、修复、格式化与类型。
- 配置写在
pyproject.toml:集中管理,避免setup.cfg、.flake8、mypy.ini分散多份。 - 分阶段引入严格规则:先
F+E,再叠加B、SIM、UP,最后考虑strict。 - 把检查前移到提交与编辑器:反馈越早,修复成本越低。
- CI 做最终把关:本地 hook 可被跳过,CI 不可。
- 定期升级工具版本:规则与性能持续改进,但升级时需复核新增告警。
- 为团队沉淀规则说明:将
# noqa与忽略规则的理由记录在贡献指南中。
下一节
继续学习:编辑器与 IDE 选择