项目用 pyproject.toml 声明元数据和依赖;新项目不写有动态安装逻辑的 setup.py
怎么检查:CI 检查仓库根目录有 pyproject.toml 且含
[project] 表;新项目出现 setup.py 时要求说明为什么、来源
为什么:pyproject.toml 是 Python 打包的标准元数据位置,各工具都读它;动态执行的 setup.py 难以静态分析,安装时还会执行任意代码。
主题 / 语言
Python 语言层的工程约定:项目与依赖管理、锁文件、版本策略、Ruff、类型检查、pytest、异步、日志、数据库访问与迁移工具、Web 框架、数据校验和打包发布。
pyproject.toml 加 uv,提交 uv.lock,CI 用 uv lock --check 和 uv sync --locked 校验,不手改锁文件。供别人安装的库只写版本范围,不把锁强加给使用方。已有 Poetry、PDM 项目不必为统一而迁移。ruff check 与 ruff format --check 必须通过;行内例外指明规则。async def 里不做阻塞调用;不需要并发收益的代码写同步。create_all 建表。[project] 表;新项目出现 setup.py 时要求说明uv lock --check 和 uv sync --locked(Poetry 项目用 poetry check --lock,PDM 项目用 pdm lock --check)uv lock --upgrade-package 名称),不手工编辑。pylock.toml,可用于导出交换,暂不要求替代 uv.lock。project.dependencies 中是否出现 == 精确版本--extra-index-url 把公网和内网索引混在一起--extra-index-url;uv 项目检查 [[tool.uv.index]] 的配置和 explicit 设置pip-audit(对 uv 项目先 uv export 出带哈希的需求文件再扫描),失败即阻断--no-index 安装并启动验证pip install --no-index --find-links 或 uv sync --offline 安装后运行冒烟测试.python-version 或 requires-python,与 Python 官方版本状态表比对,已到期的版本报错.python-version 和 requires-python);升级到新的小版本前先在 CI 里跑完整测试.python-version 存在,并定期在下一个小版本上跑一遍测试requires-python 里声明的每个小版本都要在 CI 里测试;没测过的版本不声明支持requires-python 范围内所有仍在维护的小版本ruff check 和 ruff format --check 必须在 CI 里通过;不同时用 Black、isort、flake8 等第二套格式化或 lint 工具ruff check . 和 ruff format --check .;检查仓库里是否还有 Black、isort 的配置E4, E7, E9, F, I, B, UP, SIM;服务端项目审查后加 S(安全)、ASYNC、DTZ(时区)、BLE(宽泛捕获)。# noqa 和 # type: ignorePGH004(不带规则号的 noqa)、PGH003(不带错误码的 type ignore)和 RUF100(多余的 noqa)mypy(或 pyright)并要求零错误;新代码建议 mypy 的 strict 模式Any 并写一句说明,不让 Any 扩散。py.typed 标记文件,让使用方能用到库里的类型注解py.typedpy.typed 的包会被类型检查器当作无类型。except: 和静默吞掉的 except Exception;转换异常时用 raise ... from err 保留原因E722(裸 except)、BLE001(宽泛捕获)、B904(except 中抛出新异常未带 from)、S110(try-except-pass)KeyboardInterrupt 一起捕获;不带 from 会丢掉原始异常链,排障时找不到根因。通用要求见 02。S608(疑似拼接的 SQL)找候选,人工确认pickle、yaml.load(非安全加载器)、eval 反序列化;subprocess 不用 shell=True 拼接外部输入S301(pickle)、S506(不安全的 yaml.load)、S307(eval)、S602/S604(shell=True)yaml.safe_load,命令用参数列表传给 subprocess。datetime.utcnow() 和不带时区的 datetime.now()DTZ 规则组async def 里不调用阻塞操作(同步 HTTP 库、time.sleep、同步文件和数据库调用、长时间 CPU 计算);必须调用时放到线程池或独立进程ASYNC 规则组找候选(如 ASYNC210 同步 HTTP、ASYNC251 time.sleep),评审确认;并发测试中观察事件循环延迟asyncio.to_thread 或框架提供的线程池把阻塞调用移出事件循环;CPU 密集任务用进程池或任务队列。def,框架会放到线程池执行。asyncio.TaskGroup 等结构化方式管理;create_task 创建的任务要保存引用并等待结果asyncio.create_task(...) 调用;Ruff 的 RUF006 可找出部分候选asyncio.CancelledError;捕获后做完清理要重新抛出except BaseException、except asyncio.CancelledError 的处理分支requests 时必须显式传 timeoutS113(requests 调用缺少 timeout);HTTPX 不得把超时设为 Nonehttpx.Client、create_engine 等;集成测试检查连接数logging.getLogger(__name__) 记日志,不配置处理器;应用在入口处统一配置结构化输出;生产代码不用 print 记日志T20(print)规则;评审库代码中是否调用 logging.basicConfigpydantic.v1 兼容层dict;搜索 pydantic.v1 导入metadata.create_allcreate_all 调用;CI 检查模型变化是否带了迁移文件(Django 用 makemigrations --check,Alembic 用 alembic check)create_all 只建缺少的表,不会修改已有表,也不留变更记录。迁移原则(向前修复为主、先扩后缩)见 17。src/ 目录结构,并从构建出的 wheel 安装后运行测试uv build,再在干净环境安装 wheel 跑测试没有符合条件的规则。
| 类别 | 默认 | 备选(何时用) | 不推荐 |
|---|---|---|---|
| 项目与依赖管理 | uv(pyproject.toml + uv.lock) · uv 为 MIT 或 Apache-2.0 双许可 新的应用、命令行工具和库 | Poetry(已有 Poetry 项目,团队流程稳定,不强制迁移) PDM(已有 PDM 项目) pip + pip-tools(只能用 pip 的受限环境) | 只维护松散的 requirements.txt:不锁传递依赖,安装结果不可复现 在生产机器上 pip install -U:没有记录,无法回滚 |
| 构建后端 | Hatchling | uv_build(纯 Python 项目、想与 uv 工具链保持一致时) setuptools(存量项目) maturin(用 Rust 写原生扩展) scikit-build-core(用 C/C++(CMake)写原生扩展) | 带动态安装逻辑的 setup.py:安装时执行任意代码,难以静态分析 |
| lint 与格式化 | Ruff · MIT | Black + isort + flake8(存量项目迁移期间) | 同时运行多套格式化工具:规则冲突,反复改同一段代码 |
| 类型检查 | mypy · mypy、Pyright、ty 均为 MIT | Pyright(团队主要用 VS Code(Pylance)、想让编辑器和 CI 结果一致;需要 Node 运行时) ty(只做旁路对比;官方发布稳定版后再评估是否做门禁) | 两个检查器同时做阻断门禁:规则差异导致互相冲突 |
| Web 框架 | FastAPI + Pydantic v2 + Uvicorn · FastAPI 为 MIT;Django、Flask 为 BSD-3-Clause 以 API 为主的服务 | Django(需要后台管理页面、完整的 ORM、认证和迁移一体化) Flask(小服务或存量项目) Litestar(受控试点,先验证插件和维护能力) | 自己写 ASGI 或 WSGI 框架:有成熟框架,自研维护成本高 |
| 数据校验与配置 | Pydantic v2;配置用 pydantic-settings · MIT | msgspec(性能热点路径的序列化与校验) dataclasses(不需要校验的内部数据结构) | pydantic.v1 兼容层:V1 已停止开发 每层各写一套校验器:重复且不一致 |
| 数据库访问 | SQLAlchemy 2(ORM 与 Core)+ psycopg 3 · SQLAlchemy 为 MIT;psycopg 为 LGPL-3.0(分发前按 27 的口径审查) | Django ORM(Django 项目) asyncpg(不用 ORM、追求 PostgreSQL 异步性能) SQLModel(简单的 FastAPI 服务;不当作 SQLAlchemy 的完整替代) | 自研 ORM:有成熟方案 未经测试就用于各种「兼容 PostgreSQL」的数据库:方言、类型和锁行为可能不同 |
| 数据库迁移工具 | Alembic · MIT | Django 迁移(Django 项目) | 启动时 create_all:不能修改已有表,没有变更记录 |
| HTTP 客户端 | HTTPX | requests(同步的存量项目;必须显式传 timeout) aiohttp(需要它特有的流式或 WebSocket 能力) | urllib 加手写连接池和重试:边角情况多 |
| 后台任务库 | Celery 需要路由、定时、多种消息中间件的任务系统;队列和调度的总体选型见 18 | RQ(已有 Redis、任务简单) Dramatiq(许可证审查允许 LGPL 时) | Arq 作为新项目默认:官方已声明只维护模式 FastAPI BackgroundTasks 承担不能丢的任务:进程退出即丢失 |
| 日志 | 标准库 logging + JSON 格式化器 | structlog(需要更方便地绑定上下文字段) | print 当生产日志:没有级别和结构,无法检索 |
| 测试 | pytest + Hypothesis + pytest-asyncio + testcontainers · pytest 为 MIT;Hypothesis 为 MPL-2.0 | unittest(不能引入第三方依赖的场合) pytest-xdist(测试多、需要并行;先保证测试互相隔离) | 用 SQLite 冒充 PostgreSQL:方言和锁行为不同 |
| 依赖漏洞审计 | pip-audit | OSV-Scanner(多语言仓库想用一个工具;总体选型见 25) | — |
pip install -U# noqa 或 # type: ignore 批量消除报错让 CI 变绿async def 里用 requests 或 time.sleep--extra-index-url 混用下面的内容还没核实或待补调研,不要当成规则执行。