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

uv pip-compile migration:從 requirements.txt 到可重現部署流程

·7 分鐘· loading · loading · ·
Python Uv Requirements.txt Pip-Compile Dependency-Management Deployment
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 112: 本文

featured
有些 Python 專案不是從 pyproject.toml 開始長大的。 它們可能只有幾個檔案:

requirements.txt
dev-requirements.txt
Dockerfile
README.md

一開始這很合理。 專案小,pip install -r requirements.txt 就能跑。 可是等服務上線、CI 變慢、部署需要可重現,問題就開始出現。 今天拍拍君要做的不是「把舊專案整個翻新成 uv workspace」。 那太大刀了。 我們只做一件事:

把舊的 requirements.txt / pip-compile 流程,漸進搬到 uv pip compile。 如果你還不熟 uv,可以先看 uv 完全教學。 如果你要的是 uv.lock、workspace、uv sync,可以看 uv workspace 進階。 這篇只處理一個很務實的場景:舊系統還要輸出 requirements.txt,但想讓 dependency resolution 更快、更穩、更容易 review。

一. 為什麼先從 pip-compile 遷移?
#

全新專案當然可以直接:

uv init
uv add fastapi
uv sync

但很多舊專案沒有這麼乾淨。 它們可能已經有:

  • Dockerfile 讀 requirements.txt
  • CI cache 綁 requirements.txt
  • 部署平台只吃 requirements 格式
  • 安全掃描工具只掃 requirements
  • 團隊習慣 review compiled dependency diff 這時候直接切到 uv.lock 會一次動太多地方。 比較穩的第一步是保留這個模型:
requirements.in  ->  requirements.txt

只是把產生 requirements.txt 的工具換成 uv。 部署端仍然可以跑:

pip install -r requirements.txt

或改成比較快的:

uv pip install -r requirements.txt

這就是漸進式遷移的好處。 先換 dependency workflow,不急著重寫整個部署流程。

二. 先盤點現有 requirements
#

遷移前要先知道手上的 requirements.txt 是哪一種。 第一種是手寫型:

fastapi
uvicorn
pydantic
httpx

這其實不是 lockfile。 它只是直接依賴清單。 每次安裝都有機會拿到不同版本。 第二種是 pip freeze 型:

annotated-types==0.7.0
anyio==4.9.0
fastapi==0.115.12
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
pydantic==2.11.5
starlette==0.46.2
uvicorn==0.34.3

它有鎖版本,但直接依賴與間接依賴混在一起。 半年後你很難看出 h11 是誰帶進來的。 第三種是 pip-tools 型:

# requirements.in
fastapi
uvicorn[standard]
httpx
# requirements.txt
fastapi==0.115.12
    # via -r requirements.in
httpx==0.28.1
    # via -r requirements.in
starlette==0.46.2
    # via fastapi

這是最容易搬到 uv 的狀態。 因為你已經有「人類維護 input,工具產生 output」的分工。

三. 安裝 uv,不急著改專案
#

如果還沒裝 uv:

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

macOS 也可以用 Homebrew:

brew install uv

確認版本:

uv --version

這篇用的是 uv pip 介面。 它是 pip 相容工作流,不要求你立刻變成 uv 專案。 你可以在既有 repo 裡直接跑:

uv pip compile requirements.in -o requirements.txt

拍拍君建議先開 branch:

git switch -c migrate-uv-pip-compile

然後把原本的 output 留一份:

cp requirements.txt requirements.txt.before-uv

不是因為 uv 危險。 是因為 dependency diff 很容易吵架,先留對照比較省事。

四. 建立 requirements.in
#

如果原本的 requirements.txt 是手寫型,先改名成 input。

# requirements.in
fastapi
uvicorn[standard]
httpx
python-dotenv

然後產生 output:

uv pip compile requirements.in -o requirements.txt

產出的 requirements.txt 會包含直接與間接依賴:

# This file was autogenerated by uv via the following command:
#    uv pip compile requirements.in -o requirements.txt
annotated-types==0.7.0
    # via pydantic
anyio==4.9.0
    # via
    #   httpx
    #   starlette
fastapi==0.115.12
    # via -r requirements.in
httpx==0.28.1
    # via -r requirements.in
pydantic==2.11.5
    # via fastapi
uvicorn==0.34.3
    # via -r requirements.in

以後不要手動改 requirements.txt。 要加套件,改 requirements.in。 要更新 output,重跑 uv pip compile。 這個規矩很無聊,但它救過很多專案。

五. 從 pip-tools 換過來
#

如果你原本用 pip-tools:

pip-compile requirements.in -o requirements.txt

第一步可以換成:

uv pip compile requirements.in -o requirements.txt

跑完先看 diff:

git diff -- requirements.txt

你可能會看到幾種差異:

  • 註解格式不同
  • marker 寫得更明確
  • 某些版本被重新解析
  • dependency 順序略有不同 如果第一個 PR 想降低風險,可以先拿舊 output 當 constraints:
uv pip compile requirements.in \
  --constraint requirements.txt.before-uv \
  -o requirements.txt

這樣 uv 會盡量沿用舊版本。 第一個 PR 就比較像「換編譯工具」,而不是「順便升級所有東西」。 拍拍君很推薦這個拆法。 Review 會清楚很多。

六. 拆開 production 與 dev 依賴
#

很多專案會有測試、lint、type checking 工具。 不要把它們塞進 production image。 可以整理成:

requirements.in
requirements-dev.in
requirements.txt
requirements-dev.txt

production input:

# requirements.in
fastapi
uvicorn[standard]
httpx
python-dotenv

dev input:

# requirements-dev.in
-r requirements.in
pytest
pytest-cov
ruff
mypy
types-requests

編譯:

uv pip compile requirements.in -o requirements.txt
uv pip compile requirements-dev.in -o requirements-dev.txt

安裝 production:

uv pip sync requirements.txt

安裝 dev:

uv pip sync requirements-dev.txt

uv pip sync 會移除 output 沒宣告的套件,適合追求精確環境;如果你只想在既有環境補裝依賴、不想清掉額外套件,才用 uv pip install -r ...。這樣 Docker image 不會混進測試工具,CI 也可以明確選擇要同步哪一份。

七. 改用 pyproject.toml 當 input
#

如果你想再往前一步,可以把直接依賴集中到 pyproject.toml

[project]
name = "pypy-api"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "fastapi",
  "uvicorn[standard]",
  "httpx",
  "python-dotenv",
]
[project.optional-dependencies]
dev = [
  "pytest",
  "pytest-cov",
  "ruff",
  "mypy",
]

產生 production requirements:

uv pip compile pyproject.toml -o requirements.txt

產生 dev requirements:

uv pip compile pyproject.toml \
  --extra dev \
  -o requirements-dev.txt

這種做法讓專案 metadata 比較集中。 但輸出仍然是舊部署系統熟悉的 requirements.txt。 所以它很適合過渡期。

八. constraints:控制底層版本
#

有時候你不想把間接依賴寫進 requirements.in。 例如安全公告要求 urllib3 至少某個版本:

# constraints.txt
urllib3>=2.5.0
cryptography>=45.0.0

compile 時套用:

uv pip compile requirements.in \
  --constraint constraints.txt \
  -o requirements.txt

constraints 不是「一定要安裝」。 它是「如果 dependency tree 裡出現,就必須符合條件」。 拍拍君通常會這樣分:

requirements.in          # app 直接依賴
requirements-dev.in      # 開發工具
constraints.txt          # 安全與平台限制
requirements.txt         # production output
requirements-dev.txt     # dev output

檔案看起來多一點。 但之後查 dependency conflict 會輕鬆很多。

九. hashes:等 workflow 穩了再加
#

如果部署環境需要更嚴格,可以產生 hash。

uv pip compile requirements.in \
  --generate-hashes \
  -o requirements.txt

安裝時搭配:

uv pip install --require-hashes -r requirements.txt

hash 的好處是更可驗證。 缺點是 diff 會變長。 拍拍君不建議第一個 migration PR 就把 hash 打開。 比較好的順序是:

  1. 先導入 uv pip compile
  2. 再拆 dev/prod
  3. 再整理 constraints
  4. 最後才加 hashes 每一步都能獨立 rollback。 這比一次端出一大坨 dependency diff 友善多了。

十. Docker:先保留 requirements.txt
#

舊 Dockerfile 可能是:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

第一階段可以只把 install 換成 uv。

FROM python:3.12-slim
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:0.12.9 /uv /uvx /bin/
COPY requirements.txt .
RUN uv pip sync --system --no-cache requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

--system 表示裝進 container 的系統 Python。 在 Docker 裡這很常見,因為 container 本身已經是隔離環境。 如果你想更嚴格,也可以建立 venv:

RUN uv venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN uv pip install --no-cache -r requirements.txt

兩種都可以。 小型 API image 用 --system 很乾脆。 比較複雜的 image 再考慮 venv。範例把 uv image 固定在 0.12.9;正式環境若要求供應鏈可重現,還可以進一步固定 image digest,不要長期使用會移動的 latest

十一. CI:檢查 output 有沒有忘記更新
#

遷移後最常見錯誤是:

改了 requirements.in,忘記重產 requirements.txt。 GitHub Actions 可以擋:

name: dependency-check
on:
  pull_request:
  push:
    branches: [main]
jobs:
  requirements:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
      - name: Compile requirements
        run: |
          uv pip compile requirements.in -o requirements.txt
          uv pip compile requirements-dev.in -o requirements-dev.txt
      - name: Check generated files
        run: git diff --exit-code -- requirements.txt requirements-dev.txt

這個 job 不需要安裝整個專案。 它只確認 output 是最新的。 失敗時訊息也很明確:重跑 compile,commit diff。

十二. CI cache:用 compiled output 當 key
#

測試 job 可以這樣寫:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
        with:
          python-version: "3.12"
          enable-cache: true
          cache-dependency-glob: "requirements-dev.txt"
      - run: uv pip sync --system requirements-dev.txt
      - run: pytest

setup-uv 已經能處理 uv cache;把 cache-dependency-globrequirements-dev.txt,就不必再手寫一套 actions/cache。它是完整解析後的安裝清單:requirements.in 改了但 output 沒變時,不必破壞 cache;output 真的變了,cache key 才跟著換。

十三. 升級依賴:不要全部一起升
#

新增套件時,改 input:

# requirements.in
fastapi
uvicorn[standard]
httpx
python-dotenv
sqlmodel

然後重跑:

uv pip compile requirements.in -o requirements.txt

如果只想升級某個套件:

uv pip compile requirements.in \
  --upgrade-package fastapi \
  -o requirements.txt

如果真的要全部升級:

uv pip compile requirements.in \
  --upgrade \
  -o requirements.txt

日常 PR 最好不要順手升級全世界。 新增 sqlmodel 的 PR,就讓它專心新增 sqlmodel。 安全更新 fastapi 的 PR,就讓它專心更新 fastapi。 這樣出事時才知道要 rollback 哪裡。

十四. Rollback:先準備退路
#

拍拍君建議把遷移拆成兩個 PR。 第一個 PR:

  • 建立 requirements.in
  • uv pip compile 產生 output
  • 加 CI 檢查 output
  • 部署端暫時仍用 pip install 第二個 PR:
  • Docker 改用 uv pip install
  • CI 加 uv cache
  • 比較 build time
  • 保留 rollback diff 如果第二步有問題,只回第二個 PR。 不要把 compile workflow 一起退掉。 這就是小步遷移的價值。 不是膽小,是 production 不吃熱血。

結語
#

uv pip compile 最棒的地方,不只是快。 它讓舊專案不用立刻重寫成全新 uv 專案,也能把 dependency workflow 整理乾淨。 拍拍君會記住這幾條:

  • requirements.in 放人類意圖
  • requirements.txt 放工具輸出
  • CI 檢查 output 沒有過期
  • deployment 只照 output 安裝
  • dev/prod 依賴分開
  • constraints 管安全與平台限制
  • dependency PR 不要順便升級全世界 工具遷移最怕的是把太多事情綁在一起。 先從 uv pip compile 開始,就能在不嚇壞舊系統的情況下,把速度、可重現性、review 品質慢慢帶進來。 這種改法不華麗。 但很耐用。 拍拍君覺得,耐用的東西才是真的帥。嗯,只是一點點啦。

延伸閱讀
#

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

相關文章

uv 管理 Python 版本:Install、Find、Pin、Upgrade 與直譯器選擇
·9 分鐘· loading · loading
Python Uv Python Versions Interpreter Virtualenv Developer-Tools
uv + GitHub Actions 實戰:Lockfile、Cache 與可重現 CI
·5 分鐘· loading · loading
Python Uv GitHub Actions Ci Lockfile Cache Developer-Tools
Python uv build/publish 實戰:從 wheel 到 private package workflow
·11 分鐘· loading · loading
Python Uv Packaging Wheel PyPI Private-Package Developer-Tools
Python uv scripts 實戰:PEP 723、inline dependencies 與單檔工具
·6 分鐘· loading · loading
Python Uv PEP 723 Script Developer-Tools Automation
Pandas GroupBy 實戰:聚合、Pivot Table 與報表整理
·8 分鐘· loading · loading
Python Pandas GroupBy Pivot Table Data-Analysis Reporting
Streamlit Data Editor 實戰:可編輯表格、上傳驗證與 CSV 匯入匯出
·8 分鐘· loading · loading
Python Streamlit Data-Editor CSV Validation Developer-Tools