Skip to content

Linter 与代码检查工具 ​

Linter(代码检查器)负责在运行前发现风格违规、潜在缺陷与代码异味;类型检查器(Type Checker)则在静态层面验证类型注解的一致性。Python 生态中,Ruff、Flake8、Pylint、mypy 是四款最具代表性的工具。本节将对比它们的定位、给出可复用的配置,并演示如何接入编辑器与 CI。

前置知识

阅读本节前,建议先了解:VS Code 配置 Python 开发,其中已介绍如何在编辑器中启用 Ruff。

基础概念 ​

需要先厘清几个容易混淆的概念:

术语英文职责
LinterLinter静态检查风格与潜在错误,如未使用变量、导入顺序
格式化工具Formatter只调整代码排版,不改语义,如 Black、Ruff format
类型检查器Type Checker依据类型注解推断并检查类型一致性,如 mypy、Pyright
代码异味Code Smell不影响运行但暗示设计问题的写法
规则代码Rule Code每条检查规则的短标识,如 F401、E501
内联忽略Inline Ignore用 # noqa 在当前行抑制告警

Linter 与 Formatter 不重复

Linter 会「指出问题」,Formatter 会「自动排版」。二者存在少量重叠(如行宽),实践中常见的分工是:Ruff 负责检查 + 修复 + 格式化,mypy 负责类型检查。

语法与用法 ​

安装 ​

bash
# 一次性安装,推荐放入开发依赖
pip install ruff mypy flake8 pylint

# 或使用 uv 安装为开发依赖
uv add --dev ruff mypy
bash
# 验证安装
ruff --version
mypy --version

快速上手 ​

bash
# Ruff 检查当前目录
ruff check .

# 自动修复可修复的问题
ruff check --fix .

# 格式化
ruff format .

# mypy 类型检查
mypy src/

# Flake8
flake8 .

# Pylint
pylint src/

详细说明 ​

四款工具对比 ​

维度RuffFlake8Pylintmypy
实现语言RustPythonPythonPython
检查类型风格 + 缺陷 + 导入 + 部分安全风格 + 缺陷(插件扩展)风格 + 设计 + 复杂度类型为主
速度极快(比 Flake8 快 10~100 倍)较慢慢中等
自动修复支持大量规则不支持部分不支持
可扩展性内置规则丰富,插件生态较弱插件生态成熟插件与自定义检查器插件(mypy.ini)
配置位置pyproject.toml / ruff.tomlsetup.cfg / .flake8 / tox.inipyproject.toml / .pylintrcpyproject.toml / mypy.ini
推荐度新项目首选存量项目维护需要深度设计检查时类型完备性要求高时

选择建议:

  • 新项目:Ruff(检查 + 格式化)+ mypy(类型)。
  • 存量项目:若已有大量 Flake8 配置与插件,可保留;逐步迁移到 Ruff 时可用 ruff check --select ALL 辅助发现差异。
  • 强规范团队:Pylint 的设计检查(如函数复杂度、类设计)可作为补充,但需大量调参。

Ruff 配置详解 ​

pyproject.toml:

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:

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

带类型注解的示例:

python
"""演示 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"])

运行结果:

text
$ 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):

ini
[flake8]
max-line-length = 88
extend-ignore = E203,W503
exclude = .git,__pycache__,.venv,build,dist
per-file-ignores =
    __init__.py:F401
    tests/*:E501

Pylint(pyproject.toml):

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):

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 中启用。

内联抑制告警 ​

python
"""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 后,失效的忽略会被自动发现。

实战示例 ​

案例一:为新项目搭建检查流水线 ​

项目结构:

text
myapp/
├── src/myapp/
│   ├── __init__.py
│   └── core.py
├── tests/
│   └── test_core.py
├── pyproject.toml
└── .pre-commit-config.yaml

pyproject.toml(Ruff + mypy,定位为可复用的最小配置):

toml
[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 = true

src/myapp/core.py:

python
"""核心业务逻辑"""

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)

本地执行:

bash
ruff check . && ruff format --check . && mypy src/

案例二:接入 pre-commit,在提交前自动检查 ​

.pre-commit-config.yaml:

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]

安装与运行:

bash
pip install pre-commit
pre-commit install          # 安装 git hook
pre-commit run --all-files  # 手动全量运行

此后每次 git commit 都会自动检查并修复,不合规的提交会被阻断。

案例三:GitHub Actions 中做 CI 质量闸门 ​

.github/workflows/lint.yml:

yaml
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 选择

参考链接 ​