首页
/ uv 脚本执行指南:从 uv run 到 PEP 723 内联元数据、依赖锁定与可复现性

uv 脚本执行指南:从 uv run 到 PEP 723 内联元数据、依赖锁定与可复现性

2026-09-06 11:43:36作者:凌朦慧Richard

本文基于 uv 仓库官方指南 Running scripts 整理并深入展开:介绍如何使用 uv run 免环境管理地执行 Python 脚本、通过 PEP 723 内联元数据(# /// script)声明依赖、用 shebang 创建可执行脚本、通过 uv lock --script 锁定依赖以及用 exclude-newer 保证时间维度上的可复现性。读完本文,你可以把任何一个独立的 .py 文件变成依赖自管理、Python 版本自选择、结果可复现的脚本,并理解 uv 在 crates/uv-scripts 中解析内联元数据的实际实现。

1. 为什么用 uv 运行脚本

Python 脚本(script)是用于独立执行的文件,典型用法是 python <script>.py。传统方式下,每个脚本依赖的包都要求手动创建并维护虚拟环境(venv)。uv 的做法是把环境管理交给工具本身:

  • 使用 uv run <脚本> 执行脚本时,uv 会自动为脚本准备运行环境,优先按需创建(on-demand)而不是让你维护一个长生命周期的虚拟环境;
  • 依赖声明推荐采用声明式方式——通过项目(project)或脚本内联元数据(inline metadata)声明,而不是每次命令行临时指定;
  • 如果还不熟悉 Python 环境:每个 Python 安装都带有可以安装包的“环境”,通常建议用虚拟环境隔离各脚本的依赖,uv 会自动管理这些虚拟环境。

2. 运行无依赖的脚本

2.1 最基本的运行方式

没有第三方依赖的脚本,直接用 uv run 执行:

print("Hello world")
$ uv run example.py
Hello world

只依赖标准库的脚本也无需任何额外配置:

import os

print(os.path.expanduser("~"))
$ uv run example.py
/Users/astral

2.2 向脚本传参

命令行参数会原样传给脚本,可通过 sys.argv 读取:

import sys

print(" ".join(sys.argv[1:]))
$ uv run example.py test
test

$ uv run example.py hello world!
hello world!

2.3 从 stdin 读取脚本

脚本内容还可以直接从标准输入提供:

$ echo 'print("hello world!")' | uv run -

或者在支持 here-document 的 shell 中:

uv run - <<EOF
print("hello world!")
EOF

从源码结构看,stdin 提供的脚本与磁盘脚本走的是同一套 PEP 723 元数据解析路径:crates/uv-scripts/src/lib.rs 中的 Pep723Item 枚举明确区分了 Script(磁盘脚本)、Stdin(标准输入)与 Remote(远程 URL)三种来源,其中 stdin 与远程脚本的工作目录回退为当前目录。

2.4 项目目录中运行脚本:--no-project

如果在项目(即包含 pyproject.toml 的目录)中执行 uv run,uv 会先安装当前项目再运行脚本。如果脚本本身并不依赖项目,用 --no-project 跳过该步骤:

$ # 注意:--no-project 必须放在脚本名之前
$ uv run --no-project example.py

--no-project 的 CLI 定义见 crates/uv-cli/src/lib.rs:它对应环境变量 UV_NO_PROJECT(别名 --no-workspace),行为是“不发现项目/工作区,而是在仅由 --with 依赖填充的隔离临时环境中运行”。更多关于项目的细节可参考 projects 指南

3. 运行有依赖的脚本

当脚本需要第三方包时,依赖必须安装到脚本运行的环境中。uv 不鼓励手动维护长期 venv,而是要求显式声明依赖:推荐用项目或内联元数据(见第 4 节),但也支持每次调用时临时请求依赖。

3.1 使用 --with 临时添加依赖

以依赖 rich 的脚本为例:

import time
from rich.progress import track

for i in track(range(20), description="For example:"):
    time.sleep(0.05)

不声明依赖直接运行会失败:

$ uv run --no-project example.py
Traceback (most recent call last):
  File "/Users/astral/example.py", line 2, in <module>
    from rich.progress import track
ModuleNotFoundError: No module named 'rich'

--with(可短写为 -w)请求依赖即可:

$ uv run --with rich example.py
For example: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01

需要指定版本时用 PEP 440 版本约束:

$ uv run --with 'rich>12,<13' example.py

多个依赖通过重复 --with 提供。

注意:如果 uv run 在项目中执行,这些 --with 依赖会叠加在项目依赖之上;从 CLI 定义(crates/uv-cli/src/lib.rs)可以看到,--with 依赖会安装在与项目环境隔离的临时环境层中,允许与项目依赖冲突;--with-editable--with-requirements(支持 requirements.txt、含内联元数据的 .py 文件、pylock.toml)也遵循相同的环境语义。要完全脱离项目行为,使用 --no-project

4. 声明脚本依赖:PEP 723 内联元数据

4.1 初始化脚本

Python 社区近期引入了内联脚本元数据标准格式(PEP 723),允许在脚本内部声明 Python 版本要求与依赖。使用 uv init --script 可以初始化带内联元数据的脚本:

$ uv init --script example.py --python 3.12

crates/uv-scripts/src/lib.rsPep723Script::create 实现可以看到,uv init --script 默认生成如下结构的脚本:以 requires-python = "<系统 Python 版本>"dependencies = [] 组成的元数据头,加上一个 main() 模板;若脚本已有 shebang,uv 会保留 shebang、在中间插入 # 分隔行和元数据块,并且当 shebang 不包含 uv 字样时(例如 #!/usr/bin/env python3)会发出警告,提示直接执行可能忽略内联元数据、建议改为 #!/usr/bin/env -S uv run --script

4.2 用 uv add --script 声明依赖

uv add --script 可以自动为脚本添加/更新内联元数据:

$ uv add --script example.py 'requests<3' 'rich'

执行后,脚本顶部会被写入一个 script 元数据块(TOML 格式),完整示例如下:

# /// script
# dependencies = [
#   "requests<3",
#   "rich",
# ]
# ///

import requests
from rich.pretty import pprint

resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])

之后直接 uv run example.py 即可,uv 会自动创建包含所需依赖的环境:

$ uv run example.py
[
│   ('1', 'PEP Purpose and Guidelines'),
│   ('2', 'Procedure for Adding New Modules'),
│   ('3', 'Guidelines for Handling Bug Reports'),
│   ('4', 'Deprecation of Standard Modules'),
│   ('5', 'Guidelines for Language Evolution'),
│   ('6', 'Bug Fix Releases'),
│   ('7', 'Style Guide for C Code'),
│   ('8', 'Style Guide for Python Code'),
│   ('9', 'Sample Plaintext PEP Template'),
│   ('10', 'Voting Guidelines')
]

重要:当脚本使用了内联元数据时,即使 uv run 处于项目目录内(见 在项目中运行),项目的依赖也会被忽略,此时不需要再加 --no-project

4.3 Python 版本要求

内联元数据同样支持 requires-python

# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///

# 使用 Python 3.12 新增的语法
type Point = tuple[float, float]
print(Point)

注意:dependencies 字段即使为空也必须提供(这与 crates/uv-scripts/src/lib.rsinit_metadata 默认同时生成 requires-pythondependencies = [] 两个字段的行为一致)。

uv run 会搜索并使用满足要求的 Python 版本;本地没有时会自动下载。更多细节见 Python 版本文档。

4.4 元数据块的解析规则(源码视角)

uv 对 # /// script ... # /// 块的解析实现在 crates/uv-scripts/src/lib.rsScriptTag::parse 中,其规则与 PEP 723 规范严格对齐,理解这些规则有助于写出不出错的元数据块:

  • 定位:用 memchr 子串搜索 # /// script 作为起始标记,且该标记必须出现在文件首行或紧跟换行符之后,否则视为不存在元数据块;
  • 内容提取:块内每一行必须是 # 开头的注释,# 后若有内容则第一个字符必须是空格;提取时剥掉前导 # 再拼回合法 TOML;
  • 结束标记:以最后一个恰好等于 # /// 的行为准。结束行带任何尾部内容(如 # /// unexpected 或行尾空格)会报错 UnclosedBlockTrailingContent;块内没有合法结束行则报 UnclosedBlock
  • 重复块检查:结束行之后的正文中若再出现一个完整闭合的 # /// script 块,会报 DuplicateBlock(未闭合的第二段不算重复块);
  • 多行字符串兼容:块内可以嵌入 ''' 多行字符串(如 # /// <summary> 这样的 C# 注释内容),解析器会正确将其保留在元数据中——这些边界行为在 crates/uv-scripts/src/lib.rstests 模块中有大量对应测试用例(missing_spaceno_closing_pragmaclosing_tag_trailing_contentembedded_commentunclosed_second_script_block_is_not_duplicate 等)。

元数据本身被反序列化为 Pep723Metadata 结构(crates/uv-scripts/src/lib.rs):dependencies(PEP 508 依赖列表)、requires_python(PEP 440 版本约束)、tool(可选的 [tool.uv] 表)三部分。[tool.uv] 表(ToolUv 结构,见 crates/uv-scripts/src/lib.rs)支持 indexsourcesexclude-neweroverride-dependenciesconstraint-dependencies 等大量与 pyproject.toml[tool.uv] 对齐的字段,也就是说脚本内联元数据中可以直接写 [tool.uv] 小节来覆盖索引、来源、构建约束等解析配置

5. 用 shebang 创建可执行脚本

给脚本加上 shebang 后,可以不经过 uv run 直接执行——这让位于 PATH 或当前目录的脚本可以像普通命令行工具一样使用。

创建名为 greet 的文件:

#!/usr/bin/env -S uv run --script

print("Hello, world!")

确保脚本有执行权限(例如 chmod +x greet)后直接运行:

$ ./greet
Hello, world!

shebang 形式同样支持依赖声明:

#!/usr/bin/env -S uv run --script
#
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx"]
# ///

import httpx

print(httpx.get("https://example.com"))

从源码结构看,这一形式之所以可靠,是因为 shebang 中的 env -S uv run --script 会把文件作为 PEP 723 脚本交给 uv 解析(--script 参数在 crates/uv-cli/src/lib.rs 中定义为“无论扩展名如何,都将路径解析为 PEP 723 脚本”);而 uv init 在保留非 uv shebang 时发出的警告(第 4.1 节)也正是在防止用户误用 #!/usr/bin/env python3 这类不识别内联元数据的解释器。

6. 使用替代包索引

如果需要用非默认的包索引解析依赖,可以用 --index 选项:

$ uv add --index "https://example.com/simple" --script example.py 'requests<3' 'rich'

该索引会被写入内联元数据的 [tool.uv.index] 表:

# [[tool.uv.index]]
# url = "https://example.com/simple"

对应地,运行时 uv 会从脚本元数据中收集这些索引——crates/uv-scripts/src/lib.rsindexes() 方法直接从 tool.uv 表读取 index 列表,并受 --no-sources 策略控制。若索引需要认证,参见 包索引文档。

7. 锁定脚本依赖

uv 支持使用 uv.lock 文件格式为 PEP 723 脚本锁定依赖。与项目不同,脚本必须显式执行 uv lock

$ uv lock --script example.py

uv lock --script 会在脚本旁边创建一个 .lock 文件(如 example.py.lock)。

一旦生成锁定文件,后续的 uv run --scriptuv add --scriptuv export --scriptuv tree --script 都会复用锁定的依赖版本,并在必要时更新锁文件;如果没有锁文件,uv export --script 等命令仍可正常工作,但不会创建锁文件。

从实现侧看,--locked / --frozen 等检查与脚本锁文件的联动逻辑位于 crates/uv/src/commands/project/run.rs:在脚本模式下找不到锁文件时,若用户显式传了 --locked/--frozen(CLI 来源)会直接报错并提示运行 uv lock --script;若来自环境变量 UV_LOCKED 等则只发出警告,避免破坏全局设置了该变量的用户。此外,uv audit --script 也要求脚本先锁定(见 crates/uv-cli/src/lib.rs--script 参数的文档:“The specified script must be locked, i.e. with uv lock --script <script>”)。

8. 提升可复现性:exclude-newer

除锁定依赖外,uv 支持在内联元数据的 [tool.uv] 表中使用 exclude-newer 字段,限制 uv 只考虑某个日期之前发布的分发包。这在内网、合规审计,或希望“多年后运行同一脚本得到相同依赖集”的场景中很有用:

# /// script
# dependencies = [
#   "requests",
# ]
# [tool.uv]
# exclude-newer = "2023-10-16T00:00:00Z"
# ///

import requests

print(requests.__version__)

日期需使用 RFC 3339 时间戳格式(如 2006-12-02T02:07:43Z)。在锁流程中,exclude-newer 会参与锁文件一致性检查:当锁文件记录的 exclude-newer 与本次选项不一致时,uv 判定锁需要更新(参见 crates/uv/src/commands/project/lock.rs 中的 exclude_newer 比较逻辑)。

9. 为脚本选择不同 Python 版本

uv 允许每次调用时为脚本请求任意 Python 版本:

import sys

print(".".join(map(str, sys.version_info[:3])))
$ # 使用默认 Python 版本(因机器而异)
$ uv run example.py
3.12.6
$ # 使用指定 Python 版本
$ uv run --python 3.10 example.py
3.10.15

--python(短形式 -p)支持环境变量 UV_PYTHON,其请求格式详见 Python 版本请求文档。

10. 运行 GUI 脚本

在 Windows 上,uv 会用 pythonw(无控制台窗口版本)运行扩展名为 .pyw 的脚本:

from tkinter import Tk, ttk

root = Tk()
root.title("uv")
frm = ttk.Frame(root, padding=10)
frm.grid()
ttk.Label(frm, text="Hello World").grid(column=0, row=0)
root.mainloop()
PS> uv run example.pyw

uv 使用 pythonw 运行 tkinter 脚本弹出的 Hello World 窗口

带依赖的 GUI 脚本同样支持:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QGridLayout

app = QApplication(sys.argv)
widget = QWidget()
grid = QGridLayout()

text_label = QLabel()
text_label.setText("Hello World!")
grid.addWidget(text_label)

widget.setLayout(grid)
widget.setGeometry(100, 100, 200, 50)
widget.setWindowTitle("uv")
widget.show()
sys.exit(app.exec_())
PS> uv run --with PyQt5 example_pyqt.pyw

uv 运行 PyQt5 GUI 脚本弹出的 Hello World 窗口

源码层面,.pyw 的识别与执行逻辑位于 crates/uv/src/commands/project/run.rs:uv 检测文件扩展名是否为 .pyw,然后将解释器可执行文件名从 python 替换为 pythonw 来启动进程(找不到 pythonw.exe 时回退到 python.exe);CLI 还提供了 --gui-script 参数(crates/uv-cli/src/lib.rs),可以强制把任意扩展名的路径按 PEP 723 脚本解析并用 pythonw.exe 运行,该参数仅可用于 Windows。

11. 小结与延伸阅读

把本文的关键命令汇总成速查表:

场景 命令
运行无依赖脚本 uv run example.py
脱离项目运行 uv run --no-project example.py
临时添加依赖 uv run --with 'rich>12,<13' example.py
从 stdin 运行 echo 'print(1)' | uv run -
初始化 PEP 723 脚本 uv init --script example.py --python 3.12
声明/更新脚本依赖 uv add --script example.py 'requests<3'
指定索引 uv add --index "https://example.com/simple" --script example.py rich
锁定脚本依赖 uv lock --script example.py
指定 Python 版本 uv run --python 3.10 example.py
GUI 脚本(Windows) uv run example.pywuv run --gui-script

uv run 的完整参数请参考 命令参考;接下来还可以了解如何用 uv 运行与安装工具。核心实现代码可继续深入 crates/uv-scripts/src/lib.rs(PEP 723 解析)、crates/uv/src/commands/project/run.rs(脚本运行与环境管理)以及 crates/uv-cli/src/lib.rs(CLI 参数定义)。

登录后查看全文
热门项目推荐
相关项目推荐