2972 字
15 分钟

Python 项目工程化实战(一):pyproject.toml + uv + Ruff 现代工具链从零搭建 | Python 进阶

你的 Python 项目是否还在用 setup.py + requirements.txt + .flake8 + setup.cfg + tox.ini……一堆散落的配置文件维护着?2026 年,Python 工具链已经全面现代化:

  • pyproject.toml:PEP 621 标准,一个文件统管所有配置
  • uv:Rust 编写的超快包管理器,替代 pip + venv + poetry
  • Ruff:Rust 编写的 linter+formatter,替代 flake8 + isort + Black

本文是工程化系列第一篇,带你从零搭建一个完整的现代化 Python 项目结构。


一、为什么要告别老式工具链?#

老式工具链的问题#

传统 Python 项目配置文件散落情况:
my_project/
├── setup.py # 构建配置(需要执行 Python 代码才能读取!)
├── setup.cfg # 部分配置迁移到这里
├── requirements.txt # 生产依赖
├── requirements-dev.txt # 开发依赖(手动维护,容易遗漏)
├── .flake8 # flake8 配置
├── .isort.cfg # isort 配置
├── mypy.ini # mypy 配置
├── pytest.ini # pytest 配置
├── tox.ini # tox 配置
└── pyproject.toml # 越来越多工具开始迁移到这里
# 但和其他文件并存,混乱

问题清单:

  • 配置分散在 5-8 个文件,维护成本高
  • setup.py 是可执行代码,无法静态解析,影响构建工具性能
  • requirements.txt 无法区分生产依赖/开发依赖/可选依赖
  • 缺少 lock 文件机制,pip install -r requirements.txt 结果不可复现
  • 多个 linting 工具各自跑,速度慢,配置互相干扰

现代工具链的目标#

my_project/
├── pyproject.toml # ✅ 唯一配置文件,包含所有工具配置
├── uv.lock # ✅ 自动生成的精确依赖 lock 文件
├── src/
│ └── my_project/ # ✅ src layout,避免包名冲突
│ ├── __init__.py
│ └── main.py
└── tests/
└── test_main.py

二、uv:下一代 Python 包管理器#

2.1 安装 uv#

Terminal window
# macOS / Linux(推荐,单命令安装)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows(PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# 验证安装
uv --version
# uv 0.4.x

uv 是单一二进制文件,安装后无需 Python 环境,自己管理 Python 版本。

2.2 uv 核心命令速查#

Terminal window
# ── 项目初始化 ──────────────────────────────────────────────
uv init my-project # 创建新项目(自动生成 pyproject.toml)
uv init --lib my-package # 创建库项目(src layout)
# ── Python 版本管理 ──────────────────────────────────────────
uv python install 3.12 # 安装指定 Python 版本
uv python list # 查看已安装的 Python 版本
uv python pin 3.12 # 固定当前项目使用的 Python 版本(写入 .python-version)
# ── 虚拟环境 ─────────────────────────────────────────────────
uv venv # 创建虚拟环境(自动使用项目指定的 Python 版本)
uv venv --python 3.11 # 指定 Python 版本创建
source .venv/bin/activate # Linux/macOS 激活
.venv\Scripts\activate # Windows 激活
# ── 依赖管理 ─────────────────────────────────────────────────
uv add requests # 添加生产依赖(自动更新 pyproject.toml + uv.lock)
uv add --dev pytest ruff # 添加开发依赖
uv add --optional ml numpy # 添加可选依赖组
uv remove requests # 移除依赖
uv sync # 按 uv.lock 同步环境(CI/CD 中使用)
uv sync --frozen # 严格模式:不允许更新 lock 文件(适合生产部署)
uv lock # 更新 lock 文件但不安装
uv tree # 查看依赖树
# ── 运行 ─────────────────────────────────────────────────────
uv run python main.py # 在项目虚拟环境中运行命令
uv run pytest # 运行测试
uv run -- my-cli-command # 运行项目安装的 CLI
# ── pip 兼容模式(迁移旧项目时使用)──────────────────────────
uv pip install requests # 与 pip install 语法一致
uv pip install -r requirements.txt
uv pip freeze # 输出当前环境的包列表

2.3 速度对比#

Terminal window
# 测试:安装 Django + 所有依赖
# pip(冷缓存)
time pip install django
# 约 12-15 秒
# uv(冷缓存)
time uv pip install django
# 约 1-2 秒
# uv(热缓存,已下载过)
time uv pip install django
# 约 0.1-0.3 秒(从本地缓存安装,几乎即时)

uv 使用全局缓存(~/.cache/uv/),同一个包在多个项目之间共享缓存,避免重复下载。


三、pyproject.toml:统一配置文件#

3.1 完整模板(含注释)#

# pyproject.toml —— Python 项目统一配置文件
# ── 构建系统 ──────────────────────────────────────────────────
# 声明使用哪个构建后端(用于 pip install / python -m build)
[build-system]
requires = ["hatchling"] # hatchling 是现代轻量构建后端(uv 默认使用)
build-backend = "hatchling.build"
# 其他常见选择:
# requires = ["setuptools>=61", "wheel"] # 传统方案
# requires = ["poetry-core"] # poetry 方案
# requires = ["flit_core>=3.2"] # flit 方案(简洁)
# ── 项目元信息(PEP 621 标准)────────────────────────────────
[project]
name = "my-awesome-project"
version = "0.1.0"
description = "A short description of the project"
readme = "README.md"
license = { file = "LICENSE" }
authors = [
{ name = "Your Name", email = "you@example.com" },
]
keywords = ["python", "example"]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
# 支持的 Python 版本范围
requires-python = ">=3.11"
# 生产依赖(等价于老式 requirements.txt)
dependencies = [
"requests>=2.31",
"pydantic>=2.0",
"httpx>=0.27",
]
# ── 可选依赖组(pip install my-project[ml])──────────────────
[project.optional-dependencies]
ml = [
"numpy>=1.26",
"scikit-learn>=1.4",
]
dev = [
"pytest>=8.0",
"pytest-cov>=5.0",
"ruff>=0.5",
"mypy>=1.10",
]
# ── 命令行入口点 ──────────────────────────────────────────────
[project.scripts]
# pip install 后,my-cli 命令可用
my-cli = "my_project.cli:main"
# ── 项目 URL ─────────────────────────────────────────────────
[project.urls]
Homepage = "https://github.com/yourname/my-project"
Documentation = "https://my-project.readthedocs.io"
Repository = "https://github.com/yourname/my-project"
"Bug Tracker" = "https://github.com/yourname/my-project/issues"
# ── Hatchling 构建配置 ────────────────────────────────────────
[tool.hatch.build.targets.wheel]
packages = ["src/my_project"] # src layout:指定包在 src/ 下
# ── uv 配置 ──────────────────────────────────────────────────
[tool.uv]
dev-dependencies = [ # 开发依赖(等价于 [project.optional-dependencies].dev)
"pytest>=8.0",
"pytest-cov>=5.0",
"ruff>=0.5",
"mypy>=1.10",
]
# ── Ruff:linter + formatter 配置 ────────────────────────────
[tool.ruff]
target-version = "py311" # 目标 Python 版本
line-length = 120 # 每行最大字符数(Black 默认 88,可按需调整)
src = ["src", "tests"] # 告知 Ruff 源码目录(影响 import 排序)
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes(未使用变量/导入等)
"I", # isort(import 排序)
"N", # pep8-naming(命名规范)
"UP", # pyupgrade(自动升级旧语法)
"B", # flake8-bugbear(常见 bug 模式)
"SIM", # flake8-simplify(代码简化建议)
"C4", # flake8-comprehensions(列表推导式优化)
"RUF", # Ruff 自有规则
]
ignore = [
"E501", # 行长度(由 formatter 控制,不重复报告)
"B008", # 不允许函数调用作为默认参数(FastAPI 中常用,需忽略)
]
[tool.ruff.lint.isort]
known-first-party = ["my_project"] # 声明第一方包,影响 import 排序分组
[tool.ruff.format]
quote-style = "double" # 字符串使用双引号(与 Black 默认一致)
indent-style = "space"
skip-magic-trailing-comma = false
# ── mypy 静态类型检查配置 ─────────────────────────────────────
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true # 所有函数必须有类型注解
ignore_missing_imports = true # 忽略没有 stubs 的第三方库
# ── pytest 测试配置 ───────────────────────────────────────────
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
addopts = [
"--tb=short", # 简洁的失败回溯
"--cov=src", # 覆盖率(需要 pytest-cov)
"--cov-report=term-missing",
]
# ── coverage.py 覆盖率配置 ───────────────────────────────────
[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "**/__pycache__/*"]
[tool.coverage.report]
show_missing = true
skip_covered = false

四、推荐项目结构(src layout)#

my-project/
├── pyproject.toml # 统一配置(见第三节)
├── uv.lock # 精确依赖 lock 文件(自动生成,提交到 git)
├── README.md
├── LICENSE
├── .python-version # 项目使用的 Python 版本(uv python pin 生成)
├── src/ # ← src layout 的核心:包放在 src/ 下
│ └── my_project/ # 实际的 Python 包
│ ├── __init__.py
│ ├── main.py
│ ├── cli.py # CLI 入口
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ └── utils/
│ ├── __init__.py
│ └── helpers.py
├── tests/
│ ├── conftest.py # pytest fixtures
│ ├── test_main.py
│ └── utils/
│ └── test_helpers.py
└── .github/
└── workflows/
└── ci.yml # GitHub Actions CI

为什么用 src layout?

my-project/
# 传统 flat layout(包直接放项目根目录)的问题:
# my_project/ ← 与项目根目录同名
# tests/
# 在项目根目录运行 pytest 时:
import my_project # Python 找到的是项目根目录下的 my_project/
# 而不是已安装的包,导致测试通过但实际安装失败!
# src layout 完全避免这个问题:
# src/ 目录不在 Python 路径中,必须通过 pip install -e . 安装才能 import
# 这确保测试环境和实际安装环境完全一致

五、Ruff:一个工具替代全套 lint 工具链#

5.1 安装与基础使用#

Terminal window
# 通过 uv 安装到开发依赖
uv add --dev ruff
# 或独立安装(不依赖 Python 环境)
pip install ruff
Terminal window
# Lint 检查(报告问题,不修改文件)
ruff check src/ tests/
# Lint + 自动修复(修复可自动修复的问题)
ruff check --fix src/ tests/
# 格式化(等价于 black)
ruff format src/ tests/
# 检查 + 格式化(CI 中最常用)
ruff check src/ && ruff format --check src/

5.2 常见 Ruff 规则示例#

# ── F401:未使用的 import ─────────────────────────────────────
import os # ← F401 如果 os 未在文件中使用
import sys
# ── I001:import 排序(isort 规则)──────────────────────────
# 错误:第三方库和标准库混排
import requests
import os
# 正确(ruff format 自动修复)
import os
import requests
# ── UP007:使用更简洁的 Union 写法(Python 3.10+)──────────────
from typing import Union, Optional
def func(x: Union[int, str]) -> Optional[str]: # UP007: 建议改为 int | str
...
# ruff --fix 自动改为:
def func(x: int | str) -> str | None:
...
# ── B006:可变默认参数(常见 bug!)──────────────────────────
def bad_function(data: list = []): # B006: 默认列表是共享对象!
data.append(1)
return data
def good_function(data: list | None = None): # ✅
if data is None:
data = []
data.append(1)
return data
# ── SIM108:三元表达式简化 ──────────────────────────────────
# 建议:
if condition:
x = a
else:
x = b
# 自动改为:
x = a if condition else b

5.3 与 Black/flake8 迁移对比#

Terminal window
# 原来的工具链(.pre-commit-config.yaml)
# - repo: https://github.com/psf/black
# hooks: [{ id: black }]
# - repo: https://github.com/pycqa/isort
# hooks: [{ id: isort }]
# - repo: https://github.com/pycqa/flake8
# hooks: [{ id: flake8 }]
# 三个工具串行执行,约需 20-40 秒
# 改为 Ruff(pre-commit 配置):
# - repo: https://github.com/astral-sh/ruff-pre-commit
# rev: v0.5.0
# hooks:
# - id: ruff # lint + fix
# args: [--fix]
# - id: ruff-format # format
# 单一工具,约需 0.5-2 秒

六、从零创建项目:完整流程#

Terminal window
# 1. 初始化项目
uv init --lib my-project # --lib 使用 src layout
cd my-project
# 2. 固定 Python 版本
uv python pin 3.12
# 3. 添加依赖
uv add requests pydantic httpx
uv add --dev pytest pytest-cov ruff mypy
# 4. 配置(编辑 pyproject.toml,参考第三节模板)
# 5. 检查配置
uv sync # 安装所有依赖
uv run ruff check src/ # 运行 lint 检查
uv run ruff format src/ # 格式化代码
uv run mypy src/ # 类型检查
uv run pytest # 运行测试
# 6. 查看生成的依赖 lock 文件
cat uv.lock # 包含所有依赖的精确版本(提交到 git)
# 7. 开发模式安装(使代码可以被 import)
uv pip install -e .

一键初始化脚本#

#!/bin/bash
# bootstrap.sh —— 快速初始化新 Python 项目
PROJECT_NAME=${1:-"my-project"}
# 初始化项目
uv init --lib "$PROJECT_NAME"
cd "$PROJECT_NAME"
# 固定 Python 3.12
uv python pin 3.12
# 添加开发依赖
uv add --dev \
pytest>=8.0 \
pytest-cov>=5.0 \
ruff>=0.5 \
mypy>=1.10
# 开发模式安装
uv pip install -e .
echo "✅ 项目 $PROJECT_NAME 初始化完成!"
echo "📂 目录结构:"
find . -not -path './.venv/*' -not -path './.git/*' | sort | head -30

七、CI/CD 集成(GitHub Actions)#

.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true # 启用 GitHub Actions 缓存
- name: Set up Python
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --frozen # 严格使用 lock 文件,不允许更新
- name: Lint with Ruff
run: |
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
- name: Type check with mypy
run: uv run mypy src/
- name: Run tests
run: uv run pytest --cov --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml

八、工具链对比总结#

功能老式工具现代替代优势
包安装pipuv10-100x 更快,全局缓存
虚拟环境venv / virtualenvuv venv自动集成,无需单独管理
依赖 lockpip-tools + requirements.txtuv.lock(自动)自动生成,精确可复现
依赖管理requirements.txt(手动)pyproject.toml + uv add分组管理,自动更新
Lintingflake8Ruff更快(Rust)+ 规则更多
Import 排序isortRuff(I 规则)合并到单一工具
代码格式化BlackRuff format兼容 Black,更快
类型检查mypymypy(继续使用)无需替换
项目配置setup.py + 多个 .inipyproject.toml单文件统管

下一篇预告#

Python 项目工程化实战(二):测试体系建设——pytest fixtures 进阶、参数化测试、mock/patch 最佳实践、测试覆盖率要求与 CI 门槛设置。


相关文章

本文所有工具版本信息以 2026 年 7 月为准,uv/Ruff 更新较快,请以官方文档为准。

Python 项目工程化实战(一):pyproject.toml + uv + Ruff 现代工具链从零搭建 | Python 进阶
https://971918.xyz/posts/python-guide/python-project-engineering-01/
作者
九所长
发布于
2026-07-13
许可协议
CC BY-NC-SA 4.0