RSS
菜单
全部文章快讯开发科技深度热点

代码质量与工具链(Python 从精通到入门 · 13)

内容摘要

让机器去争论格式和风格,人只争论逻辑。把 black、ruff、mypy、pre-commit 串起来,质量就不再靠自觉。

让机器去争论格式和风格,人只争论逻辑。把 black、ruff、mypy、pre-commit 串起来,质量就不再靠自觉。

你将学到

  • 用 black / ruff format 统一格式化,终结格式争论
  • 用 ruff / flake8 做 lint,用 isort 管 import 排序
  • 把 mypy 类型检查纳入本地流程
  • 用 pre-commit 在提交前自动跑检查
  • 用 pyproject.toml 统一所有工具配置
  • 在 GitHub Actions 里跑质量门禁,以及团队协作约定

前置知识

本文会把前几篇的工具串起来,建议先读 上一篇:测试 pytest 与 TDD。

一、格式化:black 与 ruff format

black 是「不妥协的代码格式化器」,只做格式,不做逻辑判断。

pip install black
black src/ tests/
black --check src/        # CI 里用:只检查不修改,有差异就退出码非 0
black --diff src/         # 显示将要做的改动
# 输入
def f( x,y ):
    return {'a':1,'b':2}

# black 之后
def f(x, y):
    return {"a": 1, "b": 2}

ruff format 是 ruff 内置的格式化器,与 black 兼容(同一套 "black 风格"),但更快:

pip install ruff
ruff format src/
ruff format --check src/

二选一:既然 ruff 的 lint 和 format 能一把梭,新项目直接上 ruff,少装一个依赖。老项目已经在用 black 就继续用,别折腾。

二、Lint:ruff 与 flake8

Lint 找的是问题:未使用的变量、可疑的写法、潜在 Bug。

flake8 是经典组合(pycodestyle + pyflakes + mccabe):

pip install flake8 && flake8 src/

ruff 是 Rust 重写的替代者,速度极快,且一个工具替代 flake8 + isort + 一大堆插件:

ruff check src/           # 检查
ruff check --fix src/     # 自动修复可修的
# ❌ ruff 会报:未使用的变量、可变默认参数
import os                     # F401 unused import

def bad(items=[]):            # B006 mutable default
    x = 1                     # F841 local variable 'x' assigned but never used
    return items

配置里选规则集:

[tool.ruff]
line-length = 88
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]   # pycodestyle/pyflakes/isort/bugbear/pyupgrade
ignore = ["E501"]                     # 行太长交给 formatter,不 lint

三、import 排序:isort

isort 把 import 按「标准库 / 第三方 / 本地」分组排序,顺序稳定。

pip install isort
isort src/
isort --check-only --diff src/
# ❌ 混乱
import requests
import os
from mypkg import db
import sys

# ✅ isort 之后
import os
import sys

import requests

from mypkg import db

已经用 ruff 的话,它的 I 规则与 isort 完全兼容,用 ruff check --fix 就够了,不必单独装 isort。

四、类型检查:mypy

改完格式和 lint,再跑类型检查(配置见 类型注解与 mypy)。

mypy src/

三者职责清晰、互不重叠:

工具 管什么 会不会改代码
formatter 换行、缩进、引号 会(自动改写)
linter 可疑写法、未用变量 部分会(--fix)
类型检查 类型不匹配 不会,只报告

五、pre-commit:提交前自动把关

本地手动跑工具,人总会忘。pre-commit 在 git commit 前自动执行,不合格就拒绝提交。

pip install pre-commit
pre-commit install       # 装到 .git/hooks,一次即可
pre-commit run --all-files   # 手动全量跑一遍

配置 .pre-commit-config.yaml:

repos:
  - repo: 
    rev: v0.6.9
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: 
    rev: v1.11.2
    hooks:
      - id: mypy
        additional_dependencies: [pydantic]

  - repo: 
    rev: v4.6.0
    hooks:
      - id: end-of-file-fixer
      - id: trailing-whitespace
      - id: check-yaml
      - id: check-added-large-files

一次 git commit 时,若 ruff-format 改动了文件就会拦截提交,git add 后重提交即可:

ruff.....................................Passed
ruff-format..............................Failed  (files were modified)

关键理念:钩子只做「快」的检查。慢的(完整测试套件)留给 CI,别让人等。

六、pyproject.toml 统一配置

把工具配置集中到一处,避免 .flake8、setup.cfg、.isort.cfg 满天飞。

[tool.ruff]
line-length = 88
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]

[tool.mypy]
python_version = "3.11"
strict = true
warn_unused_ignores = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
markers = ["slow: 慢速测试"]

[tool.coverage.run]
source = ["src"]

一份配置,所有工具读同一套标准,这是现代 Python 项目的标配。

七、CI 质量门禁:GitHub Actions

本地钩子可以 --no-verify 绕过,所以真正的防线在 CI。

# .github/workflows/quality.yml
name: quality

on:
  push:
    branches: [main]
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install deps
        run: |
          python -m pip install --upgrade pip
          pip install ruff mypy pytest pytest-cov
          pip install -e .

      - name: Lint
        run: ruff check src/ tests/

      - name: Format check
        run: ruff format --check src/ tests/

      - name: Type check
        run: mypy src/

      - name: Test
        run: pytest --cov=src --cov-fail-under=80

任何一步退出码非 0,整个 job 失败,PR 就过不了。这就是「质量门禁」:把标准写成可执行的检查,而不是写在文档里靠自觉。

八、团队协作的约定

工具解决不了所有问题,剩下的是约定:

  • 统一工具版本:把 ruff / mypy 的版本写进 pyproject 或 pre-commit 的 rev,避免各人本地结果不一致。
  • PR 必须过 CI:分支保护规则里勾选「必须通过检查才能合并」。
  • 小步提交:commit 越小,回滚和定位越容易。
  • 工具分歧不辩论:格式问题交给 formatter 决断,人在 PR 里只讨论逻辑和命名。
  • 新人第一天就 pre-commit install:把它写进 README,减少上手摩擦。

常见坑

坑 1:本地跑过了,CI 却挂

# ❌ 本地装了工具 A 的旧版本,CI 装的是新版本,规则不一致
pip install ruff        # 没锁版本
# ✅ 工具版本显式固定
pip install "ruff==0.6.9"
# 或统一走 pre-commit(rev 锁死)
pre-commit run --all-files

坑 2:格式化器与 linter 抢同一件事

# ❌ 让 linter 管行宽,又让 formatter 改行宽,来回打架
[tool.ruff.lint]
select = ["E501"]     # 行太长报错
# 但 ruff format 又可能把行排超 88 字符
# ✅ 行宽只交给 formatter,lint 里忽略 E501
[tool.ruff.lint]
ignore = ["E501"]

坑 3:pre-commit 里跑整个测试套件

# ❌ 每次提交跑 5 分钟测试,人会被逼得用 --no-verify
- id: pytest
  entry: pytest
# ✅ 钩子只跑快的静态检查,测试放 CI
- id: ruff
- id: ruff-format

小结

  • formatter(black / ruff format)解决格式,linter(ruff / flake8)找问题,mypy 查类型——职责不重叠。
  • 新项目用 ruff 一把梭:lint + format + import 排序全包,快且省依赖。
  • 配置集中到 pyproject.toml,工具版本要固定。
  • pre-commit 在提交前跑快检查,慢活留给 CI。
  • GitHub Actions 做质量门禁:任何一步失败就挡住 PR。
  • 最后靠团队约定收尾:工具定分歧,人管逻辑。

延伸阅读

  • ruff 官方文档的规则列表
  • pre-commit 官方 hook 仓库
  • 12 Factor App 中关于「开发/生产一致性」的条目

上一篇:测试 pytest 与 TDD · 下一篇:文件与路径处理

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《代码质量与工具链(Python 从精通到入门 · 13)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。