让机器去争论格式和风格,人只争论逻辑。把 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 · 下一篇:文件与路径处理
文章回复
0 条公开回复