快轉到主要內容
  1. 教學文章/

Python uv scripts 實戰:PEP 723、inline dependencies 與單檔工具

·6 分鐘· loading · loading · ·
Python Uv PEP 723 Script Developer-Tools Automation
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 69: 本文

featured

一、前言:小工具也需要可重現
#

嗨,這裡是拍拍君。Python 很適合寫小工具:檢查 API、整理 CSV、掃描檔案、產生報表、幫 repo 做一些手動維護。 這些工具常常只有一個檔案。 可是只要需要第三方套件,就會開始出現小麻煩:

python -m venv .venv
source .venv/bin/activate
pip install httpx rich typer
python check_status.py

腳本本身很好分享,但執行環境沒有跟著走。 你可以另外放 requirements.txt。 但如果只是 80 行的維護腳本,為它多開一個依賴檔,有時候又顯得太重。 uv scripts 解決的就是這件事。 你把依賴寫在 Python 檔案開頭,然後這樣執行:

uv run check_status.py

uv 會讀取腳本內的 metadata,自動建立隔離環境、安裝依賴、執行程式。 不用手動 activate。 不用叫同事猜要 pip install 什麼。 也不用把一個小工具硬升級成完整 package。 上一篇拍拍君寫過 Python uv 進階:workspace、lockfile、script 與專案管理。 那篇主要看專案工作流。 今天這篇只看一件事:

如何用 PEP 723 和 uv,把單檔 Python 腳本做得乾淨、可重現、好分享。


二、PEP 723 的 script metadata
#

PEP 723 定義了一種寫在 Python 註解裡的 metadata 格式。 最小範例長這樣:

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "httpx>=0.28.0",
#   "rich>=13.9.0",
# ]
# ///
import httpx
from rich.console import Console
console = Console()
response = httpx.get("https://example.com", timeout=10)
console.print({"status_code": response.status_code})

這段雖然是 Python 註解,但內容對 uv 來說是 TOML metadata。 uv 會知道三件事:

  • 這個腳本需要 Python 3.12 以上
  • 這個腳本需要 httpx
  • 這個腳本需要 rich 所以 .py 檔不只是程式碼。 它也帶著自己的執行說明。 這跟傳統 requirements.txt 最大的差異是位置。 requirements.txt 是「旁邊有一份依賴說明」。 PEP 723 是「腳本自己說明自己需要什麼」。 對小工具來說,這個差異很實用。 檔案移動、貼給同事、放進 scripts 目錄,都不容易漏掉依賴資訊。

三、安裝 uv,然後不要 uv init
#

如果還沒有 uv,可以用官方 installer:

curl -LsSf https://astral.sh/uv/install.sh | sh

macOS 也可以用 Homebrew:

brew install uv

確認版本:

uv --version

接著建立練習目錄:

mkdir uv-script-demo
cd uv-script-demo

這篇先不要 uv init。 這點很重要。 我們不是在建立專案。 我們是在寫「可以獨立執行的單檔工具」。 如果你的需求已經需要 pyproject.toml、uv.lock、多個模組和測試,那就應該回到 project mode。 uv scripts 很好用,但它不是拿來假裝大型專案不存在的魔法。
#

四、第一個範例:檢查網站狀態
#

建立 check_status.py:

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "httpx>=0.28.0",
#   "rich>=13.9.0",
# ]
# ///
import sys
import httpx
from rich.console import Console
from rich.table import Table
def check(url: str) -> tuple[int, float]:
    with httpx.Client(timeout=10, follow_redirects=True) as client:
        response = client.get(url)
        elapsed_ms = response.elapsed.total_seconds() * 1000
        return response.status_code, elapsed_ms
def main() -> None:
    url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com"
    status_code, elapsed_ms = check(url)
    table = Table(title="HTTP status")
    table.add_column("URL")
    table.add_column("Status", justify="right")
    table.add_column("Elapsed", justify="right")
    table.add_row(url, str(status_code), f"{elapsed_ms:.1f} ms")
    Console().print(table)
if __name__ == "__main__":
    main()

執行:

uv run check_status.py https://dailypypy.org

第一次跑時,uv 會解析 metadata、建立快取環境、下載套件。 第二次通常會快很多,因為環境和套件都已經在 uv cache 裡。 你可以把這個檔案丟給同事。 只要對方有 uv,就可以直接跑。 這比「請先建立 venv,然後 pip install 這三個套件」清楚很多。
#

五、用 uv add –script 管理依賴
#

metadata 可以手寫。 但如果你常常加減依賴,建議用 uv 指令改。 先建立空腳本:

touch fetch_json.py

加入依賴:

uv add --script fetch_json.py httpx rich

uv 會自動把 PEP 723 區塊寫進檔案。 接著補上程式:

# /// script
# dependencies = [
#   "httpx",
#   "rich",
# ]
# ///
import sys
import httpx
from rich import print
def main() -> None:
    url = sys.argv[1]
    data = httpx.get(url, timeout=10).json()
    print(data)
if __name__ == "__main__":
    main()

執行:

uv run fetch_json.py https://api.github.com/repos/astral-sh/uv

如果後來不需要 rich:

uv remove --script fetch_json.py rich

這個工作流的好處是,依賴清單不會跟 import 慢慢分離。 你修改腳本時,也順手修改它自己的 metadata。 這比「程式碼在 A,依賴記錄在 B,README 又寫了 C」可靠很多。
#

六、版本範圍怎麼寫比較剛好?
#

個人一次性腳本可以很放鬆:

# dependencies = ["httpx", "rich"]

如果腳本會留在 repo 裡,拍拍君建議至少寫下限:

# dependencies = [
#   "httpx>=0.28.0",
#   "rich>=13.9.0",
# ]

如果是團隊會反覆使用的維護工具,可以加上合理上限:

# dependencies = [
#   "httpx>=0.28.0,<1",
#   "rich>=13.9.0,<14",
# ]

這不是因為每個套件都一定會破壞相容性。 而是單檔腳本通常沒有完整測試。 少一點驚喜,對維運工具是好事。 拍拍君會這樣分:

  • 個人臨時:不鎖或只寫套件名
  • repo 維護腳本:寫下限,必要時寫上限
  • 正式部署服務:不要用單檔腳本硬扛,請建立專案並提交 lockfile 工具選擇本身也是設計。

七、把 scripts 目錄變乾淨
#

常見做法是把這類工具放進 scripts 目錄:

your-repo/
  scripts/
    check_api.py
    export_users.py
    cleanup_tmp.py
  src/
  tests/
  pyproject.toml

這些腳本可以跟主專案分開管理依賴。 例如主專案是 FastAPI,但 scripts/check_api.py 只需要 httpx 和 rich。 你就不用為了維護腳本污染 production dependencies。 範例:

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "httpx>=0.28,<1",
#   "rich>=13.9,<14",
# ]
# ///
from __future__ import annotations
import argparse
import httpx
from rich.console import Console
def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--base-url", required=True)
    parser.add_argument("--token", required=True)
    return parser.parse_args()
def main() -> None:
    args = parse_args()
    headers = {"Authorization": f"Bearer {args.token}"}
    response = httpx.get(f"{args.base_url}/health", headers=headers, timeout=10)
    response.raise_for_status()
    Console().print("[green]API is healthy[/green]")
if __name__ == "__main__":
    main()

執行:

uv run scripts/check_api.py --base-url https://api.example.com --token "$TOKEN"

這個 pattern 很適合內部工具。 主專案保持乾淨,小工具也保持可重現。 如果這支腳本未來長大到需要設定檔、資料庫、測試和 CI,那就把它升級成正式專案。 不要硬塞。
#

八、實戰範例:CSV 轉 JSONL
#

再做一個資料轉換工具。 需求很單純:

  • 讀取 CSV
  • 清掉空白欄位
  • 每列輸出成 JSONL
  • 用 typer 做 CLI
  • 用 orjson 做輸出 建立 csv_to_jsonl.py:
# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "orjson>=3.10,<4",
#   "typer>=0.15,<1",
# ]
# ///
from __future__ import annotations
import csv
from pathlib import Path
from typing import Annotated
import orjson
import typer
app = typer.Typer(no_args_is_help=True)
def clean_row(row: dict[str, str]) -> dict[str, str]:
    return {
        key.strip(): value.strip()
        for key, value in row.items()
        if key and value and value.strip()
    }
@app.command()
def convert(
    input_path: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],
    output_path: Annotated[Path, typer.Argument(dir_okay=False)],
) -> None:
    with input_path.open(newline="", encoding="utf-8") as source:
        reader = csv.DictReader(source)
        with output_path.open("wb") as target:
            for row in reader:
                target.write(orjson.dumps(clean_row(row)))
                target.write(b"\n")
    typer.echo(f"wrote {output_path}")
if __name__ == "__main__":
    app()

準備資料:

cat > users.csv <<'CSV'
name,email,role
拍拍君,pypy@example.com,admin
拍拍醬,pypychan@example.com,editor
CSV

執行:

uv run csv_to_jsonl.py users.csv users.jsonl

這就是 uv scripts 很適合的場景。 一個檔案可以說完。 依賴清楚。 介面也不差。
#

九、常見踩雷
#

第一個踩雷:把腳本寫到 800 行。 如果單檔工具已經開始有很多 helper、很多狀態、很多 domain,那就拆成專案。 第二個踩雷:依賴永遠不設版本。 短期可以,長期最好至少設下限。 第三個踩雷:忘記 requires-python。 如果你用了 Python 3.12 的語法,請寫:

# requires-python = ">=3.12"

不然同事用 3.10 跑爆,大家都很煩。 第四個踩雷:把 secret 寫進腳本。 用環境變數讀 token:

import os
token = os.environ["API_TOKEN"]

第五個踩雷:把 uv run –with 當成長期依賴管理。 如果只是今天 debug 想加 rich,可以用 –with。 如果腳本正常執行需要它,就寫進 PEP 723 metadata。
#

十、什麼時候該升級成專案?
#

適合 uv scripts:

  • 單一 .py 檔可以說完
  • 不需要被其他程式 import
  • 執行頻率不高,但希望可重現
  • 是 repo 維護、資料轉換、手動操作、demo 適合 project mode:
  • 需要多個模組
  • 需要 package metadata
  • 需要 entry points
  • 需要 lockfile 納入版本控制
  • 需要 CI、測試、部署
  • 會長期被其他程式依賴 兩者不是誰取代誰。 uv scripts 是小工具的好形狀。 project mode 是正式專案的好形狀。 你要做的是選對容器,而不是把所有東西都塞進同一個盒子。

結語
#

PEP 723 和 uv scripts 解決的是一個很日常的痛點:

我只想分享一個 Python 小工具,為什麼要順便管理一個小專案? 現在答案可以很簡單。 把依賴寫進腳本。 用 uv run 執行。 讓腳本自己帶著執行說明走。 今天拍拍君示範了:

  • PEP 723 metadata 怎麼寫
  • uv run 如何執行單檔腳本
  • uv add –script 與 uv remove –script
  • inline dependencies 的版本寫法
  • repo 裡的 scripts 工作流
  • 什麼時候該升級成正式專案 小工具不用粗糙。 它可以小,但仍然乾淨、可重現、好交接。 這就是 uv scripts 最可愛的地方。

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 69: 本文

相關文章

Python watchdog 實戰:檔案變更監控與自動化完全攻略
·8 分鐘· loading · loading
Python Watchdog Automation Filesystem Developer-Tools
Python pytest fixtures 進階:conftest、factory 與測試資料管理
·8 分鐘· loading · loading
Python Pytest Fixtures Testing Conftest Monkeypatch Developer-Tools
Python SQLAlchemy 2.0 實戰:Typed ORM、Session 與查詢模式
·9 分鐘· loading · loading
Python SQLAlchemy ORM Database SQLite Developer-Tools
FastAPI + Streamlit 實戰:API 後端與互動前端分工
·9 分鐘· loading · loading
Python Fastapi Streamlit Api Frontend Developer-Tools
Python pydantic-settings 實戰:型別安全管理 .env 與設定檔
·6 分鐘· loading · loading
Python Pydantic Pydantic-Settings Dotenv Configuration Developer-Tools
Python argparse 實戰:CLI 參數解析、旗標設計與 subcommands 完全攻略
·9 分鐘· loading · loading
Python Argparse Cli Command-Line Automation Developer-Tools