代码规范靠吼,代码审查靠肉眼,这种原始手段在项目变大之后会迅速失效。Python 生态里有一整套工具可以把这些事情自动化——提交前自动格式化、自动揪出潜在 bug、甚至在 CI 管道里卡住不合规的提交。这篇文章会从零开始,把一个空白的 Python 项目武装到牙齿,所有的配置文件和命令都是可以直接复制使用的。
一、先理清工具的角色分工
市面上 Python 的静态检查工具少说有十几种,但日常用到的核心就三个角色:格式化负责统一代码样式,静态检查负责发现无用的变量、可能的 bug 和不符合最佳实践的写法,类型检查负责校验类型注解是否正确。分工如下:
- ruff:同时兼做格式化和静态检查,用 Rust 写成,速度极快,已经逐渐取代了 flake8、isort 和 pyupgrade 的组合。
- mypy:目前 Python 类型检查的事实标准,没有之一。
- pre-commit:管理 Git 钩子的工具,可以在提交代码时自动运行上面这些检查,不通过就终止提交。
三者配合起来,开发者只需要正常写代码、正常 git commit,不合规范的地方会被自动拦截甚至自动修正。开发体验和代码质量一起拉高,不增加额外的心智负担。
二、项目初始化和工具安装
假设我们有一个刚创建的 Python 项目,结构如下:
my_project/
├── src/
│ └── __init__.py
│ └── main.py
├── tests/
│ └── __init__.py
├── pyproject.toml
└── .git/
项目已经初始化了 Git 仓库。接下来在虚拟环境里安装几个包:
# 创建并激活虚拟环境
python -m venv .venv
source .venv/bin/activate # Windows 用 .venvScriptsactivate
# 安装核心工具
pip install pre-commit ruff mypy
# 把依赖记录下来
pip freeze > requirements-dev.txt
ruff 是一个独立的命令行工具,安装好后不需要像 flake8 那样额外装配插件,它的规则集已经内置在包里。mypy 同样开箱即用。pre-commit 则是我们接下来要配置的编排器。
三、配置 ruff:格式化与检查一把抓
以前格式化用 black,检查用 flake8,排序导入用 isort,升级语法用 pyupgrade,各自一套配置文件。ruff 把这几件事统一到一个工具、一个配置文件里。在项目根目录的 pyproject.toml 中加入以下配置:
[tool.ruff]
# 目标 Python 版本
target-version = "py312"
# 行宽,与 black 默认保持一致
line-length = 100
# 启用的规则集
select = [
"E", # pycodestyle 错误
"W", # pycodestyle 警告
"F", # Pyflakes 未使用变量、语法错误等
"I", # isort 导入排序
"N", # pep8-naming 命名规范
"UP", # pyupgrade 语法升级
"B", # flake8-bugbear 常见错误
"SIM", # flake8-simplify 代码简化建议
"RUF", # ruff 自有规则
]
# 忽略的规则(按需调整)
ignore = [
"E501", # 行宽由格式化器处理,检查器不再报
"B008", # 允许在函数参数中直接使用 FastAPI 的 Depends 这类调用
]
# 格式化配置
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
解释下 select 里的几个关键项。E/W 是基础的代码风格错误,F 能发现未使用的导入和变量、语法错误等,I 自动管理导入的排序和分组,N 强制命名规范(比如类名用大驼峰),UP 会自动把旧版本写法升级为新特性(比如 Optional[str] 改成 str | None),B 会拦住容易出错的写法(比如可变默认参数),SIM 提示可以简化的表达式。
配置好后先手动跑一次,看下效果:
# 检查所有代码
ruff check src/ tests/
# 自动修复可修复的问题(包括导入排序)
ruff check --fix src/ tests/
# 格式化代码
ruff format src/ tests/
四、配置 mypy:让类型注解真正发挥作用
类型注解如果只写在代码里不检查,等于没写。mypy 的配置同样放在 pyproject.toml 里:
[tool.mypy]
python_version = "3.12"
# 严格模式,一步步开启
strict = false
# 手动开启关键的检查项
check_untyped_defs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
# 如果项目里没有类型标注的第三方库,忽略它们的报错
ignore_missing_imports = true
这里没有直接开启 strict = true,因为对于已有的老项目来说那会导致大量的错误,一步到位不现实。上面列出的各个选项是 strict 的子集,选择性地开启,既保证了核心的类型安全,又不至于让团队被报错淹没。后续可以逐步提高严格程度。
同样手动跑一次确认配置生效:
mypy src/
五、用 pre-commit 把检查变成自动关卡
到现在为止,ruff 和 mypy 都是需要手动执行的。pre-commit 的作用是在 git commit 之前自动触发它们,如果不通过,提交就会被驳回。配置文件是项目根目录的 .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0 # 改成你安装的 ruff 版本
hooks:
# 第一步:自动格式化
- id: ruff-format
args: [--config=pyproject.toml]
# 第二步:自动修复可修复的检查项
- id: ruff
args: [--fix, --config=pyproject.toml]
# 第三步:重新跑一次检查,不允许未修复的错误
- id: ruff
args: [--config=pyproject.toml]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.0 # 改成你安装的 mypy 版本
hooks:
- id: mypy
args: [--config-file=pyproject.toml]
additional_dependencies:
- types-all # 如果需要第三方类型存根
注意 ruff 的三个钩子是有顺序的。第一个格式化代码,第二个修复导入排序和简单的语法问题,第三个做纯检查——如果前两步之后还有无法自动修复的错误,这一步就会报错,阻止提交。这样开发者提交的代码必然是格式正确且没有基础错误的。
安装钩子只需要一条命令:
pre-commit install
这会在 .git/hooks/pre-commit 里生成一个脚本。之后每次执行 git commit,钩子就会自动触发。你也可以手动触发对所有文件的检查:
pre-commit run --all-files
六、实战演练:让一次有问题的提交被拦截
光说不够直观,我们故意写一段有问题的代码来跑一遍。在 src/main.py 里放入以下内容:
import os, sys, json
from typing import Optional
def fetchData(url: str, timeout = 5):
result = None
if timeout == None:
timeout = 10
print("fetching ", url, "...")
unused_variable = 123
return result
这段代码至少存在以下问题:
- 导入语句未分组排序(三个导入挤在一行)
Optional导入了但没使用(Python 3.12 可直接用| None)- 参数
timeout缺少类型注解 None的比较用了==而不是is- 存在未使用的变量
unused_variable
现在执行 git add src/main.py 然后 git commit -m "test"。pre-commit 钩子被触发,输出会显示 ruff-format 自动格式化了文件,ruff 自动修复了导入排序并把 Optional 的使用替换成 str | None,然后再次检查时发现 unused_variable 已用,但 Optional 导入仍未被清除(因为改用新语法后不再需要导入),以及 mypy 会报 timeout 参数缺少类型注解。最终提交被阻止,终端会清晰地列出所有残留问题。
修改代码解决这些问题后再次提交,钩子全部通过,提交成功。整个过程开发者只需要面对终端里的报错信息去改代码,不需要记住任何检查命令。
七、进阶:加入更多钩子堵住其他漏洞
pre-commit 的生态非常丰富,可以继续在 .pre-commit-config.yaml 里追加其他有用的钩子:
# 检查是否意外提交了大文件
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-added-large-files
args: ['--maxkb=500']
- id: check-merge-conflict # 防止合并冲突标记被提交
- id: check-yaml # 校验 YAML 文件格式
- id: end-of-file-fixer # 文件末尾自动加换行
- id: trailing-whitespace # 删除行尾空格
# 检查 poetry 或 requirements 文件是否与虚拟环境同步
- repo: https://github.com/python-poetry/poetry
rev: '1.8.0'
hooks:
- id: poetry-check
# 如果项目未使用 poetry,可省略
这些钩子几乎零配置,却能堵住很多团队协作中容易出现的低级错误,比如把几百 MB 的模型文件意外提交到 Git 仓库里。
八、把检查搬到 CI 管道里做最后一道防线
本地 pre-commit 钩子可以被开发者通过 git commit --no-verify 跳过,所以 CI 里必须再做一次独立的全量检查。以 GitHub Actions 为例,在工作流文件 .github/workflows/lint.yml 中写:
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff mypy
- name: Run ruff format check
run: ruff format --check src/ tests/
- name: Run ruff check
run: ruff check src/ tests/
- name: Run mypy
run: mypy src/
这里的 ruff format --check 只检查不修改,如果有任何格式差异就会以非零状态码退出,CI 随之失败。配合分支保护规则,任何不能通过检查的 PR 都会被拒绝合并,从流程上彻底杜绝了脏代码混入主分支的可能性。
九、这套组合在团队里怎么落地
有经验的读者可能会问:如果项目已经有几十万行代码,直接上全套规则会不会爆炸?答案是肯定会。落地策略应该分三步走:
第一步:先把 pre-commit 和 ruff format 加上,格式化是自动的,不会产生任何人工成本。启动时可以用 pre-commit run --all-files 把全项目格式化一次,然后作为一次独立的提交 push 上去。
第二步:逐步开启 ruff 的 lint 规则。用 select 只开启 E、W、F 这几个基础集,等团队适应后再加入 N、UP、B 等。每次新增规则集都伴随一次全项目的 ruff check --fix 和手动处理剩余的报错。
第三步:mypy 最后上。类型检查对代码的要求最高,可以先设为 check_untyped_defs = true 这种低门槛模式,然后渐进地开启更严格的选项。对于历史遗留的无类型代码,可以用 # type: ignore 临时压制,在后续迭代中逐个消除。
十、总结
pre-commit + ruff + mypy 的组合,是目前 Python 项目代码质量工具链里效率最高的方案之一。ruff 的使用体验比 flake8 + isort + black 的组合清爽太多,pre-commit 的钩子机制把检查动作完全融入开发流程,开发者的认知负担几乎为零。
文中的配置文件和命令都是从零搭过来的,任何一个 Python 项目复制进去就能跑通。你不需要再纠结”团队代码风格怎么统一”或者”为什么总有人在 PR 里提交未格式化的代码”这类问题——工具已经帮你把答案写好了。
把脏活累活交给机器,把有限的时间留给真正复杂的业务逻辑。这才是现代 Python 开发该有的样子。

