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 打開。 比較好的順序是:
- 先導入
uv pip compile - 再拆 dev/prod
- 再整理 constraints
- 最後才加 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-glob 綁 requirements-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 品質慢慢帶進來。 這種改法不華麗。 但很耐用。 拍拍君覺得,耐用的東西才是真的帥。嗯,只是一點點啦。