Python 写起来快,但项目一大,问题也会来得快:格式不统一、隐藏的类型错误、重复逻辑、慢查询、慢循环、没人敢改的函数。代码质量不是“感觉还行”,而是一组可以持续执行、可以在 CI 里拦截、可以用数据观察的工程实践。
这篇文章围绕 Python 代码质量的几类核心工具展开:格式化器、Linter、类型检查器、测试覆盖率工具和性能分析器。它们不应该互相替代,而应该像仪表盘一样一起工作。
质量工具各管一段路
Python 代码质量通常可以拆成几层:
- 格式一致性:交给
black、ruff format这类格式化工具,减少代码审查里的风格争论。 - 静态问题扫描:交给
ruff、flake8、pylint等 Linter,提前发现未使用变量、复杂表达式、潜在 bug。 - 类型正确性:交给
mypy、pyright,让接口约定更清楚,尤其适合多人协作和大型代码库。 - 行为正确性:交给
pytest和覆盖率工具,验证代码是否按预期运行。 - 性能瓶颈定位:交给
cProfile、py-spy、scalene等 profiler,别靠猜测优化。
关键点是:不要把所有问题都塞给一个工具。格式工具负责“长得一致”,Linter 负责“明显不对”,类型检查负责“接口契约”,测试负责“行为可信”,Profiler 负责“慢在哪里”。
一个可以直接改造的最小项目配置
下面是一个小型 Python 项目的实用配置。你可以把它复制到现有项目中,再根据团队习惯调整规则。
pyproject.toml:
[project]
name = "quality-demo"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []
[project.optional-dependencies]
dev = [
"ruff>=0.6.0",
"mypy>=1.10.0",
"pytest>=8.0.0",
"coverage>=7.5.0"
]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
ignore = []
[tool.mypy]
python_version = "3.11"
strict = true
warn_unused_ignores = true
warn_return_any = true
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.coverage.run]
branch = true
source = ["src"]
[tool.coverage.report]
show_missing = true
fail_under = 85
创建一个示例函数:
# src/pricing.py
from decimal import Decimal
def apply_discount(price: Decimal, percent: Decimal) -> Decimal:
if percent < 0 or percent > 100:
raise ValueError("percent must be between 0 and 100")
return price * (Decimal("1") - percent / Decimal("100"))
再写一个测试:
# tests/test_pricing.py
from decimal import Decimal
import pytest
from pricing import apply_discount
def test_apply_discount() -> None:
assert apply_discount(Decimal("100"), Decimal("15")) == Decimal("85.00")
def test_reject_invalid_discount() -> None:
with pytest.raises(ValueError):
apply_discount(Decimal("100"), Decimal("120"))
本地运行命令:
python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
ruff format .
ruff check .
mypy src tests
coverage run -m pytest
coverage report
如果你的项目没有使用 src 布局,需要把 source = ["src"] 和导入路径改成自己的目录,比如 app 或 package_name。
从“能跑”到“可维护”,差别在度量
代码质量管理最容易失败的地方,是只在口头上要求“写干净一点”。更有效的做法是给每一类质量问题一个可执行的检查点。
例如:
ruff check . --output-format=github
mypy src
coverage run -m pytest
coverage report --fail-under=85
python -m cProfile -o profile.out scripts/import_orders.py
这些命令分别回答不同问题:
ruff check:有没有低成本就能发现的代码问题?mypy:函数参数、返回值、对象属性是否符合约定?coverage report:测试是否覆盖了关键分支?cProfile:慢到底慢在哪个函数?
注意,覆盖率不是质量本身。85% 覆盖率的垃圾测试,仍然保护不了重构。覆盖率更像烟雾报警器:数字太低通常危险,数字很高也不代表没有火灾。
性能也属于代码质量
很多团队把性能优化放在最后,直到用户投诉才开始看日志。更稳妥的做法是:在可疑路径上保留可重复的 profiling 入口。
可以这样实践:
# scripts/profile_pricing.py
from decimal import Decimal
from pricing import apply_discount
def main() -> None:
total = Decimal("0")
for index in range(100_000):
total += apply_discount(Decimal(index), Decimal("12.5"))
print(total)
if __name__ == "__main__":
main()
运行:
python -m cProfile -s cumulative scripts/profile_pricing.py
-s cumulative 会按累计耗时排序,适合找“这个函数自己不慢,但它调用的一串东西很慢”的问题。真正优化前,先保存 profiling 结果;优化后再跑一次。没有对比数据的性能优化,经常只是把代码改复杂。
落地时别一次性拉满
如果是老项目,不建议第一天就开启最严格的规则并要求全量通过。更现实的路径是:
- 先启用格式化器,让代码风格稳定下来。
- 再启用 Linter,只拦截新代码里的明确问题。
- 类型检查从核心模块开始,不要一口气覆盖所有历史代码。
- 覆盖率门槛先设在当前水平附近,再逐步抬高。
- Profiler 用在关键路径,不要为了“看起来专业”到处测。
好的 Python 代码质量体系,不是工具列表越长越好,而是每个工具都能在正确时间给出清晰信号。格式化减少噪音,Lint 提前发现低级错误,类型检查稳住接口,测试保护行为,性能分析阻止盲目优化。把这些检查放进日常开发和 CI,代码质量才会从个人习惯变成团队能力。