本章为概念阅读 · 预计 20 分钟
这一章是纯阅读章节,没有练习题。
uv、pip、虚拟环境这些都是命令行工具,浏览器里的 Pyodide 跑不起来。这一章我们把概念、命令、最佳实践讲清楚,各位在自己本地装好 Python 环境之后实际操作一遍就能熟练。
各位有没有过这种崩溃时刻——把代码打包发给同事,同事跑了一下,报一堆 ModuleNotFoundError。「pip install 一下就好了。」「装哪个版本?」翻了翻自己的电脑,半天没找到一份完整的 requirements.txt,最后只好憋出一句:「呃,我电脑能跑啊,奇了怪了。」
这种事在 Python 圈子里实在太常见了。直到 2018 年 PEP 518 出台,2021 年 PEP 621 跟进,社区才终于约定:所有项目元数据、依赖、工具配置,统一塞进一个文件,叫 pyproject.toml。
到了 2024 年,又出了一个叫 uv 的工具,Astral 出品(就是写 ruff 那家),用 Rust 写的,比 pip 快 10 到 100 倍,单二进制,没有任何依赖,一条命令装上就能用。
这两件武器加在一起,就是这一章要讲的内容。学完之后,开新项目从零到「一个能跑、能锁定依赖、能跑测试的项目」只需要四五条命令。
先回忆一下老办法长什么样。一个「正经的」Python 项目,目录里通常有这些文件:
my-project/
├── setup.py
├── setup.cfg
├── requirements.txt
├── requirements-dev.txt
├── MANIFEST.in
├── pytest.ini
├── .flake8
├── tox.ini
└── my_project/
└── __init__.py
光配置文件就八九个。每个文件管的事都不一样:
setup.py:打包用,告诉 pip 这个项目的名字、版本、入口setup.cfg:setup.py 的一部分配置可以挪进来requirements.txt:生产依赖列表requirements-dev.txt:开发依赖列表(测试、linter、formatter)MANIFEST.in:打包时要带上哪些非代码文件pytest.ini:pytest 的配置.flake8:flake8 的配置tox.ini:多版本测试用而最惨的是:requirements.txt 不锁版本。
requests
flask
sqlalchemy
干干净净,三行搞定。半年后某天同事拉下来跑,requests 自动装了最新版,结果有个废弃的 API 被删了,代码挂掉。这就是著名的「能跑」和「能复现」之间的鸿沟。
到了 2024 年,社区终于把场子收拾干净了:
pyproject.toml(PEP 621)uv(速度王者)往下我们就一步步看,新办法到底有多省心。
pyproject.toml 是一个文件名,文件格式是 TOML。各位没接触过 TOML 的童鞋别紧张,它就是个比 JSON 友好、比 YAML 严格的配置格式。长这样:
[project]
name = "my-project"
version = "0.1.0"
中括号 [project] 是「段」,下面 key = value 是配置项。字符串用双引号,数字直接写,列表用方括号,跟大多数语言的语法差不多。
pyproject.toml 的核心思想是:项目的所有信息,集中放一个文件。具体能放什么?看下面这个完整例子:
[project]
name = "my-project"
version = "0.1.0"
description = "两点水的小工具"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.27",
"click>=8.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.5",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 100
[tool.pytest.ini_options]
testpaths = ["tests"]
各位数一下,这一个文件里同时承担了多少职责:
[project] 段:项目名字、版本、Python 版本要求、依赖列表——以前这些写在 setup.py 里[project.optional-dependencies] 段:开发依赖、可选依赖——以前是 requirements-dev.txt[build-system] 段:怎么打包这个项目——以前是 setup.py + MANIFEST.in[tool.ruff] 段:ruff 的配置——以前是 .ruff.toml 或 setup.cfg[tool.pytest.ini_options] 段:pytest 的配置——以前是 pytest.ini一个文件搞定一切。新人接手一个项目,打开 pyproject.toml,从上到下扫一遍,整个项目的元数据、依赖、工具配置全在脑子里了。
下面这段是一个项目能跑起来的最小集合:
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.27",
]
五个字段,每个都讲清楚。
name = "my-project"
项目的名字。这个名字是发布到 PyPI 时用的,全网唯一。命名规则:
-、下划线 _、点 .注意:项目名(name)和包导入名(import 用的)不一定相同。比如著名的 Pillow,项目名是 Pillow,但导入时是 import PIL;scikit-learn 项目名带连字符,导入时是 import sklearn。
version = "0.1.0"
版本号。建议遵守语义化版本(Semantic Versioning),格式是 MAJOR.MINOR.PATCH:
MAJOR:大改动、不兼容更新MINOR:新加功能,向后兼容PATCH:修 bug,向后兼容新项目从 0.1.0 起步,第一个稳定版打 1.0.0。
requires-python = ">=3.10"
声明这个项目需要哪个版本的 Python。强烈建议都写上这一行,原因有三:
版本范围语法用的是 PEP 440:
>=3.10:3.10 或更高都行>=3.10,<4.0:3.10 起,但 4.0 之前~=3.10:3.10.x 系列,不允许 3.11==3.10.*:3.10 的任意小版本2026 年开新项目,建议直接写 >=3.10 或 >=3.11。3.9 已经接近退役。
dependencies = [
"httpx>=0.27",
"click>=8.0",
]
项目运行时需要的依赖列表。每一项是一个字符串,遵守 PEP 508 语法。常见的几种写法:
dependencies = [
"httpx", # 任意版本
"httpx>=0.27", # 0.27 起
"httpx>=0.27,<1.0", # 范围
"httpx==0.27.2", # 钉死
"httpx[http2]", # 带 extras
"httpx ; python_version >= '3.10'", # 条件依赖
]
各位平时最常用的是 >=X.Y 这种「下限」写法。这是社区惯例:写下限,别写上限,除非确实知道某个上限会出问题。原因是:你今天写了 httpx<1.0,明天 httpx 1.0 出来了,所有依赖你的项目都被卡住——这个就叫「上限污染」,是个流毒。
依赖的「精确版本」靠 lock 文件(uv.lock)来记录,下面会讲。
把这五个字段拼起来,一个能跑的最小 pyproject.toml:
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.27",
]
就这。五行,比 setup.py 短得多。新人看一眼就懂。
讲完文件格式,现在轮到工具了。
uv 是 Astral 出的 Python 包管理器和项目管理器。Astral 这家公司各位应该不陌生,他们做的 ruff 现在是 Python linter 兼 formatter 的事实标准。uv 是他们的下一款产品,目标是替代 pip、pip-tools、pipenv、poetry、virtualenv、pyenv 一整套老工具。
它的卖点:
uv pip install 跟 pip install 用法一致;pyproject.toml 用的是 PEP 621 标准,迁出迁入没壁垒2024 年初发布以来,uv 已经被各大公司、开源项目快速采用。2026 年的现在,开新项目首选 uv,几乎没有疑问。
macOS 用户最简单:
brew install uv
跨平台通用方案:
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows 用户用 PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
如果各位电脑里已经有 pip:
pip install uv
但更推荐独立安装——uv 本来就是为了脱离 pip 而设计的,没必要绑着 pip 装。
装完之后看一下版本:
uv --version
输出大概是:
uv 0.5.20
很多童鞋的电脑里没有合适版本的 Python,或者只有系统自带的 Python 3.9。uv 还能帮各位装 Python:
uv python install 3.12
跑完之后,Python 3.12 就装到 uv 管理的目录里了。这相当于一个轻量版的 pyenv。
看看装了哪些 Python:
uv python list
输出大致:
cpython-3.12.7-macos-aarch64-none /Users/foo/.local/share/uv/python/...
cpython-3.11.10-macos-aarch64-none /Users/foo/.local/share/uv/python/...
cpython-3.10.15-macos-aarch64-none /Users/foo/.local/share/uv/python/...
以后开新项目直接挑想用的版本,不用纠结电脑里装了什么。
理论讲够了,开干:
uv init my-project
这一条命令做了一堆事。看看生成的目录:
cd my-project
ls -la
输出大致:
.git/
.gitignore
.python-version
README.md
main.py
pyproject.toml
逐个文件看一下。
3.12
这一个文件钉死了项目用的 Python 版本。各位到任何一台装了 uv 的电脑上,进入这个目录,uv 会自动用 3.12,没装就自动装。
[project]
name = "my-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
跟我们前面手写的最小例子很像,只多了 description 和 readme。
def main():
print("Hello from my-project!")
if __name__ == "__main__":
main()一个可以直接跑的「Hello World」入口。
uv run main.py
输出:
Hello from my-project!
uv run 这个命令是 uv 的核心入口之一,它会做这几件事:
.venv/ 虚拟环境是否存在,不存在就自动建所以从 uv init 到代码跑起来,两条命令:uv init my-project、uv run main.py。中间没有任何「先创建虚拟环境、再激活、再装依赖」的步骤。这就是 uv 的体验。
新项目跑起来了,现在加点东西进去。
老办法:
pip install httpx
# 然后手动打开 requirements.txt,加一行
echo "httpx" >> requirements.txt
两步走,而且很容易忘记第二步。uv 的办法:
uv add httpx
一条命令搞定。这条命令背后做了什么?
requires-python 的).venv/ 里httpx>=0.28 写到 pyproject.toml 的 dependencies 里uv.lock,把 httpx 和它所有传递依赖的精确版本都钉下来打开 pyproject.toml 看看:
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28",
]
dependencies 多了一行,自动添加。各位不需要手动维护这个列表。
uv add httpx click rich
一次加三个,依赖解析一起跑,比一个一个 pip install 快得多。
测试用的 pytest、linter 用的 ruff,这些不是项目运行时需要的,只在开发时用,应该放在「开发依赖」里。uv 用 --dev 标志:
uv add --dev pytest ruff
这次 pyproject.toml 多了一段:
[dependency-groups]
dev = [
"pytest>=8.3",
"ruff>=0.8",
]
注意是 [dependency-groups] 而不是 [project.optional-dependencies]——这是 PEP 735 的新格式,uv 默认使用。
uv remove rich
不光从 pyproject.toml 删除,还会从 .venv/ 卸载,并且更新 uv.lock。删得干净。
uv add httpx --upgrade
或者:
uv lock --upgrade-package httpx
升级到最新满足约束的版本。
如果对版本有特殊要求:
uv add "httpx>=0.27,<0.30"
uv run 不光能跑 main.py,能跑任何命令。
跑一段 Python 脚本:
uv run python -c "import httpx; print(httpx.__version__)"
跑 pytest:
uv run pytest
跑自定义脚本:
uv run python scripts/build.py
uv run 的好处是:它保证用的是项目的虚拟环境。各位不用 source .venv/bin/activate,不用每次新开终端都重新激活,进入项目目录之后直接 uv run 就行。
「那如果我就是想激活一下,跑一堆命令呢?」当然也可以:
source .venv/bin/activate
python -c "import httpx; print(httpx.__version__)"
pytest
激活之后跟传统流程一样。
很多包会装一个命令行工具,比如 ruff 装完会有 ruff 这个命令。在 uv 项目里这样跑:
uv run ruff check .
uv 会从 .venv/bin/ 里找 ruff 来执行。
有时候各位想用一个工具,但不想把它加进项目依赖:
uvx httpie https://httpbin.org/get
uvx 是 uv tool run 的简写,它会在一个临时环境装上 httpie,跑完即丢,不污染项目。这相当于 pipx run,但快得多。
想全局装 ruff,让任何目录都能用?
uv tool install ruff
跟 pipx install ruff 一样,但快得多。装完之后 ruff 就在 PATH 里了。
升级:
uv tool upgrade ruff
卸载:
uv tool uninstall ruff
各位可以拿 uv tool 替代 pipx、brew install 一些 Python CLI 工具。
讲到这里,关键问题来了:同事拉下来怎么跑?
老办法:
git clone <repo>
cd <repo>
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -r requirements-dev.txt
五条命令。跨平台还有差异,Windows 激活虚拟环境的命令不一样。
uv 的办法:
git clone <repo>
cd <repo>
uv sync
完事。uv sync 会做:
.python-version 指定的 Python 版本,没有就装.venv/(如果不存在)uv.lock 里钉死的精确版本装所有依赖(包括开发依赖)「精确版本」是关键。uv.lock 里记录的不是 httpx>=0.28,而是 httpx==0.28.1、加上 httpx 的所有传递依赖也都钉到具体版本。所以 uv sync 出来的环境,跟开发者电脑上的环境,字节级一模一样。
head -30 uv.lock
输出大致:
version = 1
revision = 1
requires-python = ">=3.12"
[[package]]
name = "anyio"
version = "4.6.2.post1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/.../anyio-4.6.2.post1.tar.gz", hash = "sha256:..." }
每个包记录了:
哈希值是关键——uv sync 装的时候会校验,确保下载到的包跟 lock 时一模一样,避免了「PyPI 上的包被人篡改」这种供应链攻击。
只装生产依赖,不装开发依赖(部署时常用):
uv sync --no-dev
强制重新解析依赖(比如改了 pyproject.toml 之后):
uv sync --reinstall
只装某个 dependency-group:
uv sync --only-group dev
各位平常 90% 的时间都只用 uv sync 不带任何参数,够了。
老办法的虚拟环境流程,各位都熟:
python -m venv .venv
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows
pip install ...
这一套有几个痛点:
deactivate 再激活新的uv 的策略:根本不需要激活。每次跑命令用 uv run,uv 会自动找到当前目录的 .venv/,用里面的 Python 跑。
要看当前用的是哪个 Python:
uv run which python
输出:
/Users/walter/projects/my-project/.venv/bin/python
需求:写一个「抓取一个 URL,打印响应状态码」的小工具,要求:
全套命令长这样:
# 1. 创建项目
uv init url-checker
cd url-checker
# 2. 加生产依赖
uv add httpx
# 3. 加开发依赖
uv add --dev pytest
# 4. 写代码
# 5. 跑测试
uv run pytest
# 6. 提交到 git
git add .
git commit -m "init project"
主逻辑文件 url_checker.py:
import httpx
def check_url(url: str) -> int:
"""返回 URL 的响应状态码"""
response = httpx.get(url, timeout=5.0)
return response.status_code
def main():
url = "https://httpbin.org/status/200"
code = check_url(url)
print(f"{url} -> {code}")
if __name__ == "__main__":
main()发给同事:
git push origin main
同事拉下来:
git clone <repo>
cd url-checker
uv sync
uv run url_checker.py
三条命令,环境完全一致,能跑。这就是 2026 年的工作流。
url-checker/
├── .git/
├── .gitignore
├── .python-version
├── .venv/ # uv 自动生成,不进 git
├── README.md
├── pyproject.toml
├── tests/
│ └── test_url_checker.py
├── url_checker.py
└── uv.lock
进 git 的有:
.gitignore.python-versionREADME.mdpyproject.tomluv.lock(很重要,别忘了提交)tests/url_checker.py不进 git 的:
.venv/__pycache__/、*.pyc各位常犯的错是:忘了提交 uv.lock。没有 lock 文件,uv sync 没法保证版本一致。所以记住:uv.lock 是项目的一部分,必须进 git。
各位经常听人说哪个工具好哪个工具坏。来个不带感情色彩的对比:
| 工具 | 速度 | 锁定文件 | 项目管理 | Python 管理 | 2026 推荐度 |
|---|---|---|---|---|---|
pip | 慢 | 没有(要配 pip-tools) | 没有 | 没有 | 老项目维护用 |
pipenv | 极慢 | Pipfile.lock | 有 | 没有 | 不推荐 |
poetry | 中等 | poetry.lock | 有 | 没有 | 还能用 |
uv | 极快 | uv.lock | 有 | 有 | 首选 |
如果是 2026 年开新项目,无脑选 uv;如果是老项目还在用 pip + requirements.txt,建议尽快迁过来;老项目用了 poetry 也别慌,能跑就让它跑,等下次大重构再说。
各位手上有老项目,已经在用 requirements.txt,怎么迁过来?
如果各位的 requirements.txt 已经是「输入约束」(写了 httpx>=0.27 这种范围):
uv pip compile requirements.in > requirements.txt
习惯上,requirements.in 是「输入」(带版本范围),requirements.txt 是「输出」(钉死的精确版本)。这是 pip-tools 的约定,uv pip compile 完全兼容。
这是更彻底的迁移。假设各位现有的 requirements.txt:
httpx>=0.27
click>=8.0
sqlalchemy>=2.0
第一步:在项目根目录跑:
uv init --no-package
--no-package 告诉 uv 这只是个应用,不打算发布到 PyPI。
第二步:把 requirements.txt 里的依赖一行一行 uv add:
uv add httpx click sqlalchemy
或者批量:
uv add -r requirements.txt
第三步:删掉老的 requirements.txt,需要兼容的话从 pyproject.toml 导出:
uv export --no-dev > requirements.txt
新手很容易忘记加 .venv/ 到 .gitignore。这事 uv init 已经帮你处理了,但如果是手动迁移的老项目,记得检查一下 .gitignore。
uv.lock 必须进 git。
各位有时候会习惯性 pip install something,结果装到了系统 Python 里。在 uv 项目里:
uv adduv tool installuvx不要在 uv 项目里直接 pip install。
uv 有个全局缓存,所有项目共享下载过的包:
uv cache dir
新项目第一次 uv sync,如果包在缓存里,秒装完,不用重新下载。这是 uv「快」的另一个原因。
清理缓存:
uv cache clean
平时不需要清,磁盘紧张了再清。
pyproject.toml 不光放项目元数据,还能放工具配置。完整一点的例子:
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28",
]
[dependency-groups]
dev = [
"pytest>=8.3",
"ruff>=0.8",
"mypy>=1.13",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v"
[tool.mypy]
python_version = "3.12"
strict = true
ruff、pytest、mypy 三个工具的配置全在一起,新人接手项目打开一个文件什么都看见了。
各位有时候写个一次性脚本,可能就 50 行 Python,但要用 httpx。难道为这 50 行新建一个项目?
uv 支持「内联依赖」语法(PEP 723)。在脚本头部写一段元数据:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx",
# ]
# ///
import httpx
response = httpx.get("https://httpbin.org/get")
print(response.status_code)跑:
uv run script.py
uv 会读取脚本头部的元数据,自动建一个临时环境装上 httpx,跑完即丢。单个 .py 文件就是个完整项目,发给同事一份就能跑。
这一章信息量大,最后给各位提炼成几条:
第一,pyproject.toml 取代了一切。 setup.py、setup.cfg、requirements.txt、requirements-dev.txt、pytest.ini、.flake8、tox.ini 一堆零碎文件,2026 年都可以收进 pyproject.toml 一个文件。
第二,uv 是 2026 年的首选包管理器。 Astral 出品,Rust 写的,10-100x 速度,单二进制,统一管理 Python 版本、虚拟环境、依赖。
第三,记住这五条核心命令。
uv init my-project # 创建项目
uv add httpx # 加生产依赖
uv add --dev pytest # 加开发依赖
uv sync # 同步环境(同事拉下来用)
uv run python main.py # 跑命令
第四,uv.lock 必须进 git。 这是「环境字节级一致」的保证。
第五,老项目用 uv pip compile 生成 requirements.txt,新项目用 uv add + uv sync。
「我电脑能跑你电脑跑不了」这句话,2026 年起,可以扔进历史的垃圾桶了。