uv 脚本执行指南:从 uv run 到 PEP 723 内联元数据、依赖锁定与可复现性
本文基于 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.rs 的 Pep723Script::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.rs 中init_metadata默认同时生成requires-python与dependencies = []两个字段的行为一致)。
uv run 会搜索并使用满足要求的 Python 版本;本地没有时会自动下载。更多细节见 Python 版本文档。
4.4 元数据块的解析规则(源码视角)
uv 对 # /// script ... # /// 块的解析实现在 crates/uv-scripts/src/lib.rs 的 ScriptTag::parse 中,其规则与 PEP 723 规范严格对齐,理解这些规则有助于写出不出错的元数据块:
- 定位:用
memchr子串搜索# /// script作为起始标记,且该标记必须出现在文件首行或紧跟换行符之后,否则视为不存在元数据块; - 内容提取:块内每一行必须是
#开头的注释,#后若有内容则第一个字符必须是空格;提取时剥掉前导#再拼回合法 TOML; - 结束标记:以最后一个恰好等于
# ///的行为准。结束行带任何尾部内容(如# /// unexpected或行尾空格)会报错UnclosedBlockTrailingContent;块内没有合法结束行则报UnclosedBlock; - 重复块检查:结束行之后的正文中若再出现一个完整闭合的
# /// script块,会报DuplicateBlock(未闭合的第二段不算重复块); - 多行字符串兼容:块内可以嵌入
'''多行字符串(如# /// <summary>这样的 C# 注释内容),解析器会正确将其保留在元数据中——这些边界行为在 crates/uv-scripts/src/lib.rs 的tests模块中有大量对应测试用例(missing_space、no_closing_pragma、closing_tag_trailing_content、embedded_comment、unclosed_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)支持 index、sources、exclude-newer、override-dependencies、constraint-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.rs 的 indexes() 方法直接从 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 --script、uv add --script、uv export --script、uv 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
带依赖的 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
源码层面,.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.pyw 或 uv 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 参数定义)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

