用 uv 和 PEP 723 写一个自带依赖的 Python 单文件脚本
很多自动化脚本一开始只是一个 tools.py,后来慢慢加上 requests、rich、pandas 这类依赖。问题也随之出现:脚本发给别人以后,对方不知道该装哪些包;服务器上临时执行时,还要先创建虚拟环境。
uv 支持 PEP 723 内联脚本元数据,可以把 Python 版本和依赖直接写在 .py 文件顶部。本文用一个可运行的 daily_report.py 示例,演示如何把脚本做成“复制即运行”的工具。
完成后,你会得到:
- 一个带内联依赖的 Python 单文件脚本
- 一条
uv run daily_report.py运行命令 - 一个可选的脚本锁文件
- 几个常见报错的直接修复方法
适合什么场景
这种方式适合小而独立的脚本:
- 日志分析、CSV 清洗、数据报表
- 运维巡检、定时任务、一次性迁移
- 分享给同事的小工具
- 不值得创建完整项目目录的自动化脚本
如果脚本已经发展成多模块项目,或者需要打包发布到 PyPI,建议继续使用 pyproject.toml 和普通 uv 项目。单文件脚本的优势是轻量和易分享,不是替代所有项目结构。
方法 1:临时指定依赖运行脚本
先看最简单的方式:不改脚本,只在运行时告诉 uv 需要哪些包。
新建 hello_rich.py:
from rich.console import Console
console = Console()
console.print("[green]hello from uv[/green]")
直接运行会失败,因为系统环境里不一定安装了 rich:
python3 hello_rich.py
常见报错类似:
ModuleNotFoundError: No module named 'rich'
用 uv run --with 可以临时创建隔离环境并运行:
uv run --with rich hello_rich.py
这种方式适合一次性测试。如果这个脚本要长期保存或分享,最好把依赖写进脚本本身。
方法 2:把依赖写进脚本
下面做一个更完整的例子:读取 tasks.csv,按状态统计任务数量,并用表格输出结果。
1. 准备 uv
如果还没有安装 uv,先执行:
curl -LsSf https://astral.sh/uv/install.sh | sh
重新打开终端后检查:
uv --version
uv help
2. 创建脚本文件
初始化一个带脚本元数据的文件:
uv init --script daily_report.py --python 3.12
添加这个脚本需要的依赖:
uv add --script daily_report.py rich python-dateutil
执行后,uv 会在 daily_report.py 顶部写入类似这样的元数据块:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "python-dateutil",
# "rich",
# ]
# ///
这个块就是 PEP 723 的核心:依赖跟着脚本走,不再需要额外发送 requirements.txt。
实现:编写 daily_report.py
把 daily_report.py 改成下面的完整内容:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "python-dateutil",
# "rich",
# ]
# ///
from __future__ import annotations
import argparse
import csv
from collections import Counter
from pathlib import Path
from dateutil.parser import parse
from rich.console import Console
from rich.table import Table
console = Console()
def load_tasks(path: Path) -> list[dict[str, str]]:
with path.open(newline="", encoding="utf-8") as file:
return list(csv.DictReader(file))
def build_table(tasks: list[dict[str, str]]) -> Table:
status_counts = Counter(task["status"] for task in tasks)
due_dates = [parse(task["due"]) for task in tasks if task.get("due")]
next_due = min(due_dates).date().isoformat() if due_dates else "n/a"
table = Table(title="Daily Task Report")
table.add_column("Metric")
table.add_column("Value", justify="right")
table.add_row("Total tasks", str(len(tasks)))
table.add_row("Open", str(status_counts["open"]))
table.add_row("Doing", str(status_counts["doing"]))
table.add_row("Done", str(status_counts["done"]))
table.add_row("Next due date", next_due)
return table
def main() -> None:
parser = argparse.ArgumentParser(description="Build a small task report.")
parser.add_argument("csv_file", type=Path, help="CSV file with title,status,due columns")
args = parser.parse_args()
tasks = load_tasks(args.csv_file)
console.print(build_table(tasks))
if __name__ == "__main__":
main()
再准备一份测试数据:
cat > tasks.csv <<'EOF'
title,status,due
Write blog,done,2026-06-02
Check server,open,2026-06-03
Clean logs,doing,2026-06-04
Update docs,open,2026-06-05
EOF
运行脚本:
uv run daily_report.py tasks.csv
第一次运行时,uv 会读取脚本顶部的依赖,创建隔离环境,然后执行脚本。后续再运行会复用缓存,速度会快很多。
验证输出
正常输出应该类似下面这样:
Daily Task Report
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Metric ┃ Value ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total tasks │ 4 │
│ Open │ 2 │
│ Doing │ 1 │
│ Done │ 1 │
│ Next due date │ 2026-06-02 │
└───────────────┴────────────┘
还可以查看这个脚本的依赖树:
uv tree --script daily_report.py
如果你要把脚本放进生产环境或定时任务,建议生成锁文件:
uv lock --script daily_report.py
这会在旁边生成:
daily_report.py.lock
提交脚本时可以一起提交锁文件,让后续执行尽量复现相同依赖版本。
做成可执行命令
如果希望直接执行 ./daily_report.py,可以在文件第一行加 shebang:
#!/usr/bin/env -S uv run --script
完整文件开头应该像这样:
#!/usr/bin/env -S uv run --script
#
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "python-dateutil",
# "rich",
# ]
# ///
然后加执行权限:
chmod +x daily_report.py
./daily_report.py tasks.csv
这对放在 ~/bin、项目 scripts/ 目录、CI 任务和服务器巡检脚本都很方便。
常见问题
ModuleNotFoundError
原因通常是直接用了 python daily_report.py,绕过了 uv。
修复方式:
uv run daily_report.py tasks.csv
如果脚本里还缺依赖,用下面命令加入元数据:
uv add --script daily_report.py package-name
Python 版本不匹配
如果脚本写了:
# requires-python = ">=3.12"
但机器上没有合适版本,先安装:
uv python install 3.12
uv run daily_report.py tasks.csv
在项目目录里运行时依赖不对
带内联元数据的脚本会使用脚本自己的依赖,而不是当前项目的依赖。这样做的好处是脚本更独立;如果它确实需要项目代码,就应该改成普通项目命令,而不是单文件脚本。
权限不足
如果执行 ./daily_report.py 提示没有权限:
chmod +x daily_report.py
如果系统不支持 env -S,退回显式命令最稳:
uv run --script daily_report.py tasks.csv
总结
uv 加 PEP 723 很适合管理小型 Python 自动化脚本:依赖写在文件里,运行时由 uv 自动创建隔离环境。对于需要分享、定时运行或临时复制到服务器上的脚本,这比手工维护虚拟环境和 requirements.txt 更轻。实际使用时,建议先用 uv add --script 管理依赖,再用 uv lock --script 固定生产脚本的依赖版本。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/self-contained-python-scripts-uv.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。