uv 系列(二):创建现代 Python 项目——pyproject.toml与项目结构

核心目标 :能够区分脚本、应用与可发布包,使用 uv 创建结构清晰的项目,并理解 pyproject.toml.python-version.venvuv.lock 各自负责什么。

前置知识:已完成 Part 1,能够运行 uv --versionuv python list

验证环境:uv 0.11.29、Python 3.12、Windows 11 PowerShell。最后复核日期:2026-07-20。


2.1 先选项目类型,再谈目录结构

Python 代码并不都需要被打成包。uv 的 init 将常见场景明确分开:

场景 推荐命令 是否默认构建 典型用途
单文件脚本 uv init --script task.py 自动化、数据处理、一次性任务
普通应用 uv init --app app-name Web 服务、内部应用
可安装 CLI uv init --package cli-name 命令行工具
可发布库 uv init --lib lib-name 被其他项目导入的库

应用不一定需要作为 Python 包安装;库则必须能被构建为 wheel/sdist。--package 可以理解为"创建一个带命令入口的打包项目"。
#mermaid-svg-xsephwyXJOOuYnTP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xsephwyXJOOuYnTP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xsephwyXJOOuYnTP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xsephwyXJOOuYnTP .error-icon{fill:#552222;}#mermaid-svg-xsephwyXJOOuYnTP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xsephwyXJOOuYnTP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xsephwyXJOOuYnTP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xsephwyXJOOuYnTP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xsephwyXJOOuYnTP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xsephwyXJOOuYnTP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xsephwyXJOOuYnTP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xsephwyXJOOuYnTP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xsephwyXJOOuYnTP .marker.cross{stroke:#333333;}#mermaid-svg-xsephwyXJOOuYnTP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xsephwyXJOOuYnTP p{margin:0;}#mermaid-svg-xsephwyXJOOuYnTP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xsephwyXJOOuYnTP .cluster-label text{fill:#333;}#mermaid-svg-xsephwyXJOOuYnTP .cluster-label span{color:#333;}#mermaid-svg-xsephwyXJOOuYnTP .cluster-label span p{background-color:transparent;}#mermaid-svg-xsephwyXJOOuYnTP .label text,#mermaid-svg-xsephwyXJOOuYnTP span{fill:#333;color:#333;}#mermaid-svg-xsephwyXJOOuYnTP .node rect,#mermaid-svg-xsephwyXJOOuYnTP .node circle,#mermaid-svg-xsephwyXJOOuYnTP .node ellipse,#mermaid-svg-xsephwyXJOOuYnTP .node polygon,#mermaid-svg-xsephwyXJOOuYnTP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xsephwyXJOOuYnTP .rough-node .label text,#mermaid-svg-xsephwyXJOOuYnTP .node .label text,#mermaid-svg-xsephwyXJOOuYnTP .image-shape .label,#mermaid-svg-xsephwyXJOOuYnTP .icon-shape .label{text-anchor:middle;}#mermaid-svg-xsephwyXJOOuYnTP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xsephwyXJOOuYnTP .rough-node .label,#mermaid-svg-xsephwyXJOOuYnTP .node .label,#mermaid-svg-xsephwyXJOOuYnTP .image-shape .label,#mermaid-svg-xsephwyXJOOuYnTP .icon-shape .label{text-align:center;}#mermaid-svg-xsephwyXJOOuYnTP .node.clickable{cursor:pointer;}#mermaid-svg-xsephwyXJOOuYnTP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xsephwyXJOOuYnTP .arrowheadPath{fill:#333333;}#mermaid-svg-xsephwyXJOOuYnTP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xsephwyXJOOuYnTP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xsephwyXJOOuYnTP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xsephwyXJOOuYnTP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xsephwyXJOOuYnTP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xsephwyXJOOuYnTP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xsephwyXJOOuYnTP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xsephwyXJOOuYnTP .cluster text{fill:#333;}#mermaid-svg-xsephwyXJOOuYnTP .cluster span{color:#333;}#mermaid-svg-xsephwyXJOOuYnTP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xsephwyXJOOuYnTP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xsephwyXJOOuYnTP rect.text{fill:none;stroke-width:0;}#mermaid-svg-xsephwyXJOOuYnTP .icon-shape,#mermaid-svg-xsephwyXJOOuYnTP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xsephwyXJOOuYnTP .icon-shape p,#mermaid-svg-xsephwyXJOOuYnTP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xsephwyXJOOuYnTP .icon-shape .label rect,#mermaid-svg-xsephwyXJOOuYnTP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xsephwyXJOOuYnTP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xsephwyXJOOuYnTP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xsephwyXJOOuYnTP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





代码只有一个文件?
--script
需要被其他项目安装或发布?
--app
提供终端命令?
--package
--lib

选错并非不可修复,但会带来误导。例如没有 [build-system] 的应用在 uv sync 时不会安装当前项目;此时配置 [project.scripts] 也不会凭空生成命令。


2.2 创建 weather-cli

本文使用一个可安装 CLI 作为贯穿示例:

powershell 复制代码
uv init --package --name weather-cli --python 3.12 weather-cli
Set-Location weather-cli

uv 0.11.29 默认得到类似结构:

text 复制代码
weather-cli/
├── .git/
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
└── src/
    └── weather_cli/
        └── __init__.py

首次执行项目命令后还会出现:

text 复制代码
├── .venv/
└── uv.lock

发行名称 weather-cli 使用连字符,导入包 weather_cli 使用下划线,这是正常且常见的映射。

2.2.1 四个关键文件的职责

文件 谁维护 是否提交 Git 职责
pyproject.toml 开发者和 uv 项目元数据、直接依赖、工具配置
uv.lock uv 解析后的精确、跨平台依赖集合
.python-version 开发者/uv 通常是 本地默认 Python 请求
.venv/ uv 当前机器可执行环境

一句话记忆:pyproject.toml 说"项目允许什么",uv.lock 说"这次解析选择什么",.venv 是"本机实际装了什么"。


2.3 读懂 pyproject.toml

整理后的配置如下:

toml 复制代码
[project]
name = "weather-cli"
version = "0.1.0"
description = "A small weather query command-line application"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
authors = [
    { name = "Example Maintainer", email = "maintainer@example.com" },
]
dependencies = []

[project.scripts]
weather = "weather_cli.cli:main"

[build-system]
requires = ["uv_build>=0.11.29,<0.12.0"]
build-backend = "uv_build"

2.3.1 [project] 是标准元数据

这一段来自 Python 打包标准(PEP 621),不是 uv 私有格式。因此构建后端、包索引和其他兼容工具都能理解它。

  • name:发行名称,在包索引和安装命令中使用;
  • version:发行版本,必须符合 Python 版本规范;
  • requires-python:消费者可使用的 Python 范围;
  • dependencies:包运行所需的直接依赖;
  • readmelicenseauthors:进入构建元数据。

不要把 Ruff、pytest 等开发工具写入 dependencies,否则用户安装 CLI 时也会被迫安装它们。

2.3.2 [project.scripts] 定义终端命令

toml 复制代码
[project.scripts]
weather = "weather_cli.cli:main"

等号左侧是用户执行的命令,右侧是 模块:可调用对象。它不是 shell 别名;构建并安装项目后,安装器会创建跨平台入口脚本。

2.3.3 [build-system] 选择构建后端

toml 复制代码
[build-system]
requires = ["uv_build>=0.11.29,<0.12.0"]
build-backend = "uv_build"

构建前端负责发起构建,构建后端负责把源代码变成 sdist/wheel。uv build 是前端命令,uv_build 是本文选择的后端。也可以在初始化时选择 Hatchling、Flit、PDM、setuptools、Maturin 等后端:

powershell 复制代码
uv init --lib --build-backend hatch example-lib

选型应基于项目需求,不要仅为"流行"频繁更换后端。构建后端是发布链路的一部分。

2.3.4 [tool.uv] 只放 uv 扩展

标准元数据解决不了的 uv 行为放在 [tool.uv],例如默认依赖组:

toml 复制代码
[tool.uv]
default-groups = ["dev", "test"]

其他工具使用各自命名空间:[tool.ruff][tool.pytest.ini_options]。不要把所有配置都堆进 [tool.uv]


2.4 为什么可发布包优先使用 src/ 布局

如果包目录直接位于仓库根目录,开发时的当前工作目录可能让 import weather_cli 成功,即便项目根本没有正确安装。测试通过后,构建产物却可能缺文件。

src/ 布局要求解释器通过安装后的包导入,更接近用户真实环境:

text 复制代码
weather-cli/
├── src/
│   └── weather_cli/
│       ├── __init__.py
│       ├── cli.py
│       └── service.py
└── tests/
    └── test_service.py

它不能替代构建产物测试,但能提前暴露很多路径错误。


2.5 编写最小可运行 CLI

src/weather_cli/service.py

python 复制代码
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class Weather:
    city: str
    temperature_c: float
    condition: str


def format_weather(weather: Weather) -> str:
    """Format weather data for terminal output."""
    return (
        f"{weather.city}: {weather.temperature_c:.1f} °C, "
        f"{weather.condition}"
    )

src/weather_cli/cli.py

python 复制代码
from __future__ import annotations

import argparse
from collections.abc import Sequence

from weather_cli.service import Weather, format_weather


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Show example weather data")
    parser.add_argument("city", nargs="?", default="Shanghai")
    return parser


def main(argv: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    weather = Weather(args.city, 26.0, "clear")
    print(format_weather(weather))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

入口函数返回整数退出码,方便测试和 shell 自动化。业务格式化逻辑与参数解析分离,后续接入 HTTP 客户端时不会把所有代码塞进 main()

运行:

powershell 复制代码
uv run weather Beijing

第一次运行时,uv 会创建 uv.lock、创建 .venv、安装当前项目,然后执行入口命令。源代码以 editable 方式安装,修改 .py 文件后通常无需重复安装。


2.6 添加测试与质量工具

开发依赖使用标准依赖组:

powershell 复制代码
uv add --group test pytest
uv add --dev ruff

配置结果类似:

toml 复制代码
[dependency-groups]
dev = ["ruff>=0.12.0"]
test = ["pytest>=8.0"]

示例中的下界由实际 uv add 结果决定,不要从文章复制未来可能过时的工具版本。dev 默认会同步,其他组需显式请求,或者配置 default-groups

tests/test_service.py

python 复制代码
from weather_cli.service import Weather, format_weather


def test_format_weather() -> None:
    weather = Weather("Shanghai", 26.04, "clear")

    assert format_weather(weather) == "Shanghai: 26.0 °C, clear"

执行:

powershell 复制代码
uv run --group test pytest
uv run ruff format --check .
uv run ruff check .

如果希望测试组默认存在:

toml 复制代码
[tool.uv]
default-groups = ["dev", "test"]

之后 uv syncuv run pytest 会包含两组。团队应明确默认组,避免某位开发者"本地碰巧装过 pytest"掩盖配置缺失。


2.7 应用项目为何可能不安装自身

执行:

powershell 复制代码
uv init --app simple-app

普通应用默认没有 [build-system]。uv 会管理它的依赖和环境,但 uv sync 不会把项目自身安装为包。这适合直接运行 main.py

powershell 复制代码
uv run main.py

如果应用后来需要稳定的包导入、命令入口或构建产物,应显式转为打包项目,而不是在 sys.path 上打补丁:

  1. 增加合适的 [build-system]
  2. 把包代码放进 src/<import_name>/
  3. 配置 [project.scripts]
  4. 运行 uv syncuv build 验证。

2.8 配置发现与优先级

uv 可以从项目级 pyproject.toml/uv.toml、用户级配置、环境变量和命令行读取设置。排障时采用以下思路:

  1. 命令行是否显式传入参数;
  2. 是否设置 UV_* 环境变量;
  3. 当前目录向上发现了哪个项目;
  4. 项目或用户配置是否覆盖默认值。

可用参数减少"在错误目录执行"的风险:

powershell 复制代码
uv --project C:\work\weather-cli run weather Shanghai
uv --directory C:\work\weather-cli run pytest

--project 指定项目发现位置;--directory 先改变工作目录。两者语义不同,不要仅凭名字猜测。


2.9 常见错误

错误一:ModuleNotFoundError,但在仓库根目录直接运行又正常

这是典型的导入路径假阳性。使用 src/ 布局、声明构建系统,并通过 uv run 执行:

powershell 复制代码
uv sync
uv run python -c "import weather_cli; print(weather_cli.__file__)"

错误二:配置了 [project.scripts],命令仍不存在

检查项目是否声明 [build-system],以及入口模块和函数是否真实存在。然后运行:

powershell 复制代码
uv sync
uv run weather --help

错误三:把 pytest 放进 project.dependencies

使用 uv remove pytest,再执行 uv add --group test pytest。运行依赖描述用户安装项目所需的内容,测试工具只属于开发过程。

错误四:修改 pyproject.toml 后手工改 uv.lock

锁文件由 uv 维护:

powershell 复制代码
uv lock
uv lock --check

错误五:提交 .venv

从 Git 索引中移除它,并确认 .gitignore 包含 .venv。不要把删除环境当作丢失依赖;依赖可以从声明和锁文件重建。


2.10 项目验收清单

  • uv run weather Shanghai 在未激活环境时成功。
  • uv run --group test pytest 通过。
  • uv run ruff format --check .uv run ruff check . 通过。
  • python -c "import weather_cli" 只在正确环境中成功。
  • pyproject.toml 只包含直接依赖,开发工具位于 dependency groups。
  • uv.lock 已提交,.venv 未提交。
  • 包名、导入名、CLI 命令名的区别已记录在 README。
  • requires-python 与真实 CI 测试范围一致。

2.11 本篇小结

现代 Python 项目的核心不是一个漂亮目录,而是让标准元数据、构建系统、源代码、锁文件和环境各司其职。src/ 布局与 uv run 让本地执行更接近用户安装后的真实情况,dependency groups 则防止开发工具污染运行依赖。

下一篇将深入 uv add → resolve → uv.lock → sync 的状态链路,并解释锁文件、依赖分组、升级和冲突诊断。

官方参考

相关推荐
林泽毅6 小时前
PyTRIO快速入门(一):概念、推理与训练
人工智能·python·深度学习·机器学习·语言模型
admin and root6 小时前
「移动安全」安卓APP 反编译&frida脱壳技巧分享
android·开发语言·python·web安全·微信小程序·移动安全·攻防演练
雪的季节7 小时前
Python基础5-18
开发语言·python
学术小李7 小时前
基于Pytorch,如何用CUDA自己写算子?(一)
人工智能·pytorch·python
ShirleyWang0128 小时前
让headlamp控制台能访问
linux·服务器·python·k8s·k3s
想会飞的蒲公英8 小时前
用 Streamlit 给文本分类模型做一个演示页面
人工智能·python·机器学习
老徐聊GEO8 小时前
亲测有效的AI品牌检测公司案例分享
大数据·人工智能·python
华研前沿标杆游学8 小时前
2026年企业对标学习TOP7项目:小米数字化考察上榜
python
大模型码小白9 小时前
Spring AI 框架实战:Java 后端集成大模型的架构设计与工程落地
java·人工智能·python·spring
BerryS3N9 小时前
深度解析:从零构建生产级大模型 RAG(检索增强生成)系统全栈指南
开发语言·python·ai