Web Day 2 環境與工具鏈:uv、VS Code、Git 與專案結構
執行需求:CPU 可跑。昨天我們跑起了第一個 FastAPI 服務,但環境是用臨時的 pip 弄起來的,這在寫一支小程式時沒問題,可是一旦要寫一個要長期維護、要跟別人協作的專案,就需要一套可重現、可分享、可驗證的工具鏈。今天要把這套工具鏈一次建好:uv 管理 Python 版本與套件、VS Code 提供編輯體驗、Git 守住版本歷史、最後用一個 hello-api 範例專案把所有東西串起來。
引言
後端工程師最常踩的坑,不是框架學不來、不是演算法想不出來,而是「環境」。明明在自己電腦跑得好好的程式,到了同事的電腦就壞;在 staging 跑得好好的服務,到了正式機就掛;部署說明寫了 pip install -r requirements.txt,可是隔了六個月再回來裝,套件已經不相容了。這些問題的根源都是「環境描述不夠嚴謹」:沒有鎖定 Python 版本、沒有鎖定套件版本、沒有把設定檔放進版控。
2025 年的 Python 生態系已經有很成熟的工具可以解決這些問題。本系列選擇 uv 作為環境與套件管理工具,它是用 Rust 寫的極速安裝器,速度比 pip 快上十倍以上;同時它整合了 Python 版本管理(類似 pyenv)、虛擬環境管理(類似 venv)、套件管理(類似 pip + poetry)的功能,是 Astral 團隊(就是開發 ruff 的那個團隊)在 2024 到 2025 年陸續推出的主流工具。配合 VS Code 的 Python 擴充套件與 Git,整個開發體驗會非常順暢。
今天的目標是建立一套「換台電腦只要 30 秒就能還原」的環境。我們會建立一個 hello-api 專案,包含標準的目錄結構、pyproject.toml、.python-version、.gitignore,並用 uv 安裝依賴、用 httpx 寫一支簡單的測試驗證一切就緒。學完之後,未來每天寫的範例都會沿用這套結構。
為什麼選 uv 而不是 pip + venv
傳統的 Python 工作流是這樣的:用 pyenv(或系統 Python)選定版本、用 venv 建立虛擬環境、用 pip 安裝套件、用 requirements.txt 鎖定版本。這套流程本身沒有問題,但工具之間要記住的指令很多、要在不同工具之間切換、且 pip 的安裝速度在大型專案上會讓人等到不耐煩。
uv 把這些功能整合進單一指令:uv python install 裝 Python、uv venv 建虛擬環境、uv add 加套件、uv lock 產生 lock 檔。它的設計哲學是「一個檔案、一個指令」就能處理大部分事情,並且所有操作都比傳統工具快上一個量級。在 2025 年 7 月的主流版本(uv 0.7)裡,鎖定檔 uv.lock 採用跨平台的格式,能確保不同作業系統的開發者裝到完全相同的版本組合。
另外,uv 跟 FastAPI、Pydantic、SQLModel 等主流套件的合作非常好,這幾個專案的官方教學在 2024 到 2025 年間都陸續加入 uv 的推薦。我們從這個系列的第一個專案就採用 uv,後續 Day 6、Day 17 還會看到它跟資料庫、測試整合的情境。
安裝 uv 與選定 Python 版本
不同作業系統的安裝方式略有不同,我們用各平台通用的官方腳本:
# macOS 與 Linux(官方推薦的一行安裝指令)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows(PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# 安裝完成後,重新開啟終端機讓 PATH 更新
uv --version
# 輸出:uv 0.7.x(實際版本會依釋出時間略有不同)
如果你的環境已經有 Python 3.13,uv 也會直接使用系統的版本;如果你想完全用 uv 管理的 Python,可以執行 uv python install 3.13 讓它下載一個隔離的版本。對這系列來說,Python 3.13 是基準版本,後續所有的範例都用這個版本測過。
確認環境:
# 列出 uv 知道的 Python 版本
uv python list
# 輸出(節錄):
# cpython-3.13.5-macos-x86_64 /Users/me/.local/share/uv/python/cpython-3.13.5-macos-x86_64
# cpython-3.13.5-linux-x86_64 /home/me/.local/share/uv/python/cpython-3.13.5-linux-x86_64
# cpython-3.13.5-windows-x86_64 C:\Users\me\AppData\Roaming\uv\python\cpython-3.13.5-windows-x86_64
# 安裝 Python 3.13
uv python install 3.13
# 確認目前預設的 Python 版本
uv python find
# 輸出(範例):/Users/me/.local/share/uv/python/cpython-3.13.5-macos-x86_64/bin/python3
uv python list 會列出 uv 知道的所有版本與路徑,方便你確認目前系統有幾個 Python 可用;uv python install 3.13 則會下載並註冊一個隔離的版本,避免污染系統 Python。uv python find 是拿來快速確認「目前這個目錄會用哪個 Python」的好工具,因為它會讀目錄裡的 .python-version 檔案。
VS Code 與推薦擴充套件
VS Code(Visual Studio Code)是這系列推薦的編輯器,但本系列並不強迫你用哪一套編輯器;只要能編輯 .py 檔、能跑命令列、能看到 git diff,都可以用。不過如果你還沒選好編輯器,VS Code 在 2025 年仍然是 Python 後端開發的主流選擇,原因是它的 Python 擴充套件做得非常好。
推薦安裝的擴充套件:
| 擴充套件 | 用途 |
|---|---|
| Python(ms-python.python) | Python 語言伺服器、IntelliSense、除錯、Lint |
| Pylint / Ruff | 語法檢查(ruff 是用 Rust 寫的極速 linter) |
| Even Better TOML | pyproject.toml 編輯支援 |
| REST Client | 直接從 VS Code 對 API 發請求(可用 .http 檔) |
| GitLens | 強化 Git 整合(看 commit、blame、分支比較) |
| Docker | Day 30 開始會用到 |
安裝完擴充套件後,第一次打開 Python 檔案 VS Code 可能會提示你選 Python 解譯器,這時選 uv python find 印出來的那個路徑就好。如果你用 uv venv 建立虛擬環境,VS Code 通常會自動偵測到 .venv/bin/python 並提示你切換。
Git 與版本控制
Git 是後端工程師的基本工具,每個專案都應該從第一天就放進版控。常見的迷思是「等我寫完再 commit」,這會讓你失去中間過程的所有脈絡。除錯時最常後悔的事,就是「啊,我昨天改壞的那段程式碼忘了 commit」。
建議的 commit 顆粒度:每一個能獨立運作的小段落就 commit 一次。例如「新增 hello world 端點」、「加入 /health 健康檢查」、「升級 fastapi 0.116」。這樣未來要回溯時,可以精準定位到某一個改動。Commit message 用中文或英文都可以,重點是「為什麼這次要改」,而不是「改了什麼」(diff 自己會顯示)。
建立 .gitignore 是另一個第一天要做的事,.venv/、__pycache__/、*.db 這些都應該排除,避免把環境資料、編譯暫存、SQLite 資料庫推到版控。
專案結構與 pyproject.toml
這系列採用的專案結構是後端社群的主流寫法:
# hello-api 專案結構
hello-api/
├── .gitignore
├── .python-version # 記錄這個專案用哪個 Python 版本
├── pyproject.toml # 套件與工具設定
├── README.md
├── src/
│ └── hello_api/
│ ├── __init__.py
│ └── main.py # FastAPI 應用本體
└── tests/
└── test_smoke.py # 最小的煙霧測試
把程式碼放在 src/hello_api/ 而非直接在專案根目錄,是社群近幾年逐漸接受的寫法,目的是避免「不小心 import 到專案根目錄的其他檔案」。這種 import 行為在開發階段會讓你以為程式能跑,但實際上依賴路徑非常脆弱;放在 src/ 底下強制你用正式的方式安裝與 import,能在早期就抓到路徑問題。
pyproject.toml 是現代 Python 專案的單一設定檔,PEP 621 在 2021 年標準化後,幾乎所有主流工具(build、uv、poetry、pdm)都採用這個格式。我們的 hello-api 範例設定檔長這樣:
[project]
name = "hello-api"
version = "0.1.0"
description = "FastAPI hello world 範例"
requires-python = ">=3.13"
dependencies = [
"fastapi==0.116.0",
"uvicorn[standard]==0.35.0",
]
[project.optional-dependencies]
dev = [
"httpx==0.28.0",
"pytest==8.4.0",
"ruff==0.7.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 100
target-version = "py313"
[tool.pytest.ini_options]
testpaths = ["tests"]
這份設定檔做了幾件事:宣告專案基本資訊(名稱、版本、Python 版本需求);列出執行期依賴(fastapi、uvicorn)與開發依賴(httpx、pytest、ruff);設定 build system 用 hatchling;給 ruff 與 pytest 一些常用參數。鎖定版本號(例如 fastapi==0.116.0)是為了確保不同人裝到相同的版本,這也是 uv 在 uv.lock 裡幫你做的事。
完整實作:建立 hello-api 專案
把今天所有東西一次做完:建立 hello-api 專案、安裝依賴、啟動伺服器、用 httpx 測試、git 初始化並 commit。整個流程大約五到十分鐘,是 Web Day 35 開始做「預約管理系統」前的標準暖身。
先確認 uv 已裝好:
# 確認 uv 已就緒
uv --version
# 輸出:uv 0.7.x
# 確認目前的 Python
uv python find
# 輸出:...cpython-3.13.x...(實際路徑會依作業系統不同)
建立專案目錄並用 uv 初始化:
# 建立 hello-api 專案
mkdir hello-api
cd hello-api
# 初始化專案(互動式詢問時可直接按 Enter 採用預設值)
uv init --package --python 3.13
# 會自動產生 pyproject.toml、hello_api/__init__.py、hello_api/main.py 等檔案
# 加入執行期依賴
uv add "fastapi==0.116" "uvicorn[standard]==0.35"
# 加入開發依賴
uv add --dev "httpx==0.28" "pytest==8.4" "ruff==0.7"
# 同步依賴並產生 uv.lock
uv sync
# 輸出:Resolved 14 packages in 0.42s
# Installed 14 packages in 0.31s
uv init --package 會自動產生套件結構(包含 src/<name>/ 與可安裝的設定),這是社群推薦的寫法。uv add 不只安裝套件,還會把套件名稱與版本寫進 pyproject.toml 並更新 uv.lock。uv sync 則是依據 uv.lock 把所有依賴一次裝好,是切換到新機器後必跑的指令。
把 main.py 改成昨天的 hello world 範例:
# src/hello_api/main.py
from datetime import datetime, timezone
from fastapi import FastAPI
app = FastAPI(title="hello-api", version="0.1.0")
@app.get("/")
def read_root():
return {"message": "Hello, FastAPI!", "service": "hello-api"}
@app.get("/health")
def health_check():
return {"status": "ok"}
@app.get("/now")
def now():
current = datetime.now(timezone.utc).isoformat()
return {"now_utc": current}
啟動伺服器,並在另一個終端機跑測試:
# 在 hello-api 專案根目錄啟動伺服器
uv run uvicorn hello_api.main:app --reload --port 8000
# 啟動成功後看到:
# INFO: Uvicorn running on http://127.0.0.1:8000
uv run 會自動啟用專案虛擬環境並執行指令,所以不必手動 source .venv/bin/activate。這是 uv 比傳統 venv 方便很多的地方:你在哪個目錄執行 uv run,它就自動切到那個專案的環境。
用 httpx 寫煙霧測試:
# tests/test_smoke.py
import httpx
def test_root():
with httpx.Client(base_url="http://127.0.0.1:8000", timeout=5.0) as client:
r = client.get("/")
assert r.status_code == 200
assert r.json() == {"message": "Hello, FastAPI!", "service": "hello-api"}
def test_health():
with httpx.Client(base_url="http://127.0.0.1:8000", timeout=5.0) as client:
r = client.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_now():
with httpx.Client(base_url="http://127.0.0.1:8000", timeout=5.0) as client:
r = client.get("/now")
assert r.status_code == 200
body = r.json()
assert "now_utc" in body
# 時間字串必須能被 datetime 解析
datetime.fromisoformat(body["now_utc"])
這三個測試分別覆蓋三個端點。用 httpx(而不是 FastAPI 的 TestClient)的原因是:等 Web Day 16 介紹 FastAPI 內建測試時,你會發現兩種寫法各有用途。今天先用外部 client 模擬「另一支程式真的打過來」的情境,這在部署後用監控腳本驗證服務是否活著特別有用。
執行測試(在伺服器還在跑的狀態下):
# 在 hello-api 專案根目錄
uv run pytest -v
# 輸出(範例):
# tests/test_smoke.py::test_root PASSED
# tests/test_smoke.py::test_health PASSED
# tests/test_smoke.py::test_now PASSED
# ====== 3 passed in 0.42s ======
最後建立 .gitignore 並初始化 git:
# 檢視 .gitignore(uv init 預設會建立一份)
cat .gitignore
# 應該至少包含:
# .venv/
# __pycache__/
# *.pyc
# *.db
# dist/
# build/
# .pytest_cache/
# 初始化 git 並做第一次 commit
git init
git add .
git commit -m "建立 hello-api 專案骨架(FastAPI + uv + pytest)"
# 輸出(範例):
# [main (root-commit) 0a8b3c1] 建立 hello-api 專案骨架(FastAPI + uv + pytest)
# 12 files changed, 184 insertions(+)
uv init 預設就會建立一份完整的 .gitignore,不需要自己手寫。整個流程跑完,你會得到一個能跑、有測試、有版控、可重現的 hello-api 專案。從這裡開始,後續每天寫的範例都會沿用相同的結構。
常見錯誤與踩雷
第一個常見的問題是「uv 找不到 Python」。症狀是 uv python install 3.13 失敗、或是 uv python find 印出系統 Python 而非 uv 管理的版本。原因通常是 PATH 沒設定好,安裝完 uv 後必須關閉再開終端機才會生效。如果你用的是 Windows,安裝後 PATH 會自動更新,但某些 shell 視窗不會重新載入,這時候重新開一個新的 PowerShell 視窗通常就解決。
第二個常見的踩雷是「uv.lock 應該 commit 進版控嗎?」答案是要。uv.lock 跟 package-lock.json(npm)或 go.sum(Go)一樣,是用來確保不同機器裝到完全相同套件版本的鎖定檔。即使你正在開發函式庫(library),也建議把 lock 檔 commit 進去;如果是部署應用程式(application),那就更一定要。這跟「相依套件版本應該鎖定」是同一件事。
第三個是「裝飾器寫錯位置」延續到今天的 @app.get。在 hello-api 的結構裡,這個裝飾器寫在 hello_api/main.py 內,如果你把 app 物件放在另一個檔(例如 hello_api/app.py),則 uvicorn 啟動指令要對應改成 uvicorn hello_api.app:app。把 app 物件放在哪個檔、用什麼變數名稱都可以,但要記得啟動指令的 module:variable 形式要對應好。
效能與實務提醒
uv 的速度優勢在大型專案會特別明顯。一份有 200 個套件的 requirements.txt,pip 第一次安裝可能要三分鐘,uv 通常在十秒內完成。這個差距在 CI/CD(持續整合)環境特別有感,因為每次部署都會重新安裝依賴。
VS Code 的 Python 擴充套件預設會跑 Pylint 或 Ruff 做靜態檢查,可能會在背景稍微消耗 CPU。如果你用比較舊的電腦,可以把它設定為只在儲存檔案時執行,而不是每次打字時都跑:
// .vscode/settings.json(放在專案根目錄,會跟著版控走)
{
"python.linting.enabled": true,
"python.linting.ruffEnabled": true,
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit"
}
}
}
這份設定放在 .vscode/settings.json 會跟著版控走,所有協作者都能享受到同樣的設定;個人偏好(例如字型大小、配色)放在「使用者設定」而非「工作區設定」,避免影響別人。
另一個提醒:建立 .venv 在專案根目錄是有意為之的,這樣 VS Code、編輯器、其他工具都能自動找到它。但 .venv 通常很大(數百 MB),所以一定要放進 .gitignore。在某些 CI/CD 環境(例如 GitHub Actions)會建議把 .venv 加進快取(cache),可以省下不少安裝時間。
進階工具:pyproject 與鎖定檔的協作
在前面的範例中,uv add 會自動更新 pyproject.toml 與 uv.lock。但實務上有兩個情境要手動處理:升級套件版本、換到新機器同步環境。下面用 Python 腳本示範幾個常用動作,幫你熟悉工具鏈的整體操作。
# manage_deps.py
# 用 uv 的 subprocess API 管理專案依賴
import subprocess
def uv_run(*args: str) -> str:
"""執行 uv 指令並回傳標準輸出"""
result = subprocess.run(
["uv", *args],
capture_output=True,
text=True,
check=True,
)
return result.stdout
# 1. 列出專案依賴
print("=== 專案依賴 ===")
print(uv_run("tree"))
# 2. 升級單一套件到最新版
# uv_run("add", "--upgrade", "fastapi")
# 3. 同步依賴(換機器後必跑)
# uv_run("sync")
# 4. 移除不需要的套件
# uv_run("remove", "some-package")
這支腳本用 subprocess.run 呼叫 uv 指令。capture_output=True 把 stdout 與 stderr 收進變數;check=True 在指令失敗時拋例外。實務上你會用 uv tree 來看依賴樹,這對「為什麼會裝到某個間接依賴」的除錯很有用。本系列後續的章節會需要 uv sync 把 lock 檔還原成環境;遇到「同事的電腦能跑、我這邊不行」的問題時,第一步就是 uv sync。
第二個進階工具是寫一個「專案健康度檢查器」,把所有該有的檔案與設定一次確認:
# health_check.py
# 確認 hello-api 專案的關鍵檔案與設定都到位
from pathlib import Path
REQUIRED_FILES = [
"pyproject.toml",
"uv.lock",
".gitignore",
".python-version",
"src/hello_api/__init__.py",
"src/hello_api/main.py",
"tests/test_smoke.py",
]
def main():
missing = []
for name in REQUIRED_FILES:
if not Path(name).exists():
missing.append(name)
if missing:
print("缺少以下檔案:")
for f in missing:
print(f" - {f}")
else:
print("專案結構完整,所有檔案就位")
# 額外檢查:.venv 是否在 .gitignore
gitignore = Path(".gitignore").read_text(encoding="utf-8")
if ".venv/" in gitignore:
print(".venv 已從版控排除")
else:
print("警告:.venv 不在 .gitignore 裡,會被誤推到版控")
main()
這支腳本可以放在 CI 流程的最前面跑,當作「部署前的把關」。如果有任何檔案缺失,就不要繼續部署。這種「fail fast」的設計,能在早期就抓到問題,避免到部署後段才發現「啊,少了一個 .gitignore 把整個 .venv 推上去了」。本系列後續的章節會把這種「自我檢查」的設計擴充成完整的部署前置腳本。
進階工作流:分支策略與 commit 訊息
建立環境之後,最重要的就是「怎麼跟 Git 共處」。前一節提過「頻繁 commit」,這裡再深入一點談分支策略。對小型後端專案,推薦的工作流是「GitHub Flow」的簡化版:main 分支永遠是可上線狀態,新功能或修 bug 從 main 開新分支,PR 審查通過後合併回 main。
# git_workflow.py
# 用 Python 模擬常見的 git 工作流(實際用命令列即可,這裡示範語意)
def fake_git_workflow():
steps = [
"git checkout main",
"git pull origin main",
"git checkout -b feature/add-jwt",
"# ... 寫程式 ... ",
"git add .",
'git commit -m "feat(auth): 加入 JWT 認證相依性"',
"git push origin feature/add-jwt",
"# ... 開 PR,等審查通過 ... ",
"git checkout main",
"git pull origin main",
"git branch -d feature/add-jwt",
]
for i, step in enumerate(steps, 1):
print(f"{i:>2}. {step}")
fake_git_workflow()
這段偽陽紀事不是真的可以執行,而是展示「一個完整的 Git 工作流長什麼樣」。重點是步驟 6 的 commit 訊息:feat(auth): 加入 JWT 認證相依性 採用「Conventional Commits」格式——前綴標明類型(feat、fix、chore、docs、refactor)、括弧標明範圍(auth、api、db)、冒號後面是簡短說明。這種格式讓 git log 變得易讀,未來也方便自動產生 CHANGELOG。
另一個常見的 Git 技巧是「互動式 rebase 整理 commit」。開發過程中你可能會留下 10 個小 commit,但合併到 main 前希望整理成 3 個有意義的 commit,這時 git rebase -i HEAD~10 就能做到。本系列不深入 Git 的所有細節,但請記得:commit 是給未來的自己(與同事)看的訊號,整理過的歷史是給未來的禮物。
對完全沒用過 Git 的讀者,建議裝 GitHub Desktop 或 Sourcetree 等 GUI 工具先熟悉觀念,再回到命令列。GitHub Desktop 在 macOS 與 Windows 都有,Sourcetree 則是跨平台。GUI 工具能幫你視覺化 branch 與 commit 的關係,比一開始就面對一堆抽象的 SHA-1 雜湊友善得多。
最後補充一個 Python 小工具,幫你在每次 commit 前自動跑 linter 與型別檢查。這種「自動化把關」的概念,是後續 Web Day 32 介紹 CI/CD 時的基礎。
# pre_commit.py
# commit 前自動跑 ruff 與基本檢查
import subprocess
def run(cmd: list[str]) -> bool:
"""執行指令;失敗回傳 False"""
print(f"$ {' '.join(cmd)}")
return subprocess.run(cmd).returncode == 0
def main():
ok = True
# 1. 檢查 ruff(linter)
ok = ok and run(["ruff", "check", "src/", "tests/"])
# 2. 自動修補可修的問題
ok = ok and run(["ruff", "check", "--fix", "src/"])
# 3. 檢查格式化
ok = ok and run(["ruff", "format", "--check", "src/"])
# 4. 確認必要檔案
for f in ["pyproject.toml", ".gitignore", "README.md"]:
import os
if not os.path.exists(f):
print(f"缺少 {f}")
ok = False
if ok:
print("所有檢查通過,建議 commit")
else:
print("有問題,請修正後再 commit")
main()
這支腳本是「pre-commit hook」的 Python 寫法。在正式專案裡,你會改用 pre-commit 套件(在 .pre-commit-config.yaml 設定)來跑這些步驟;本系列 Day 32 會示範完整的 GitHub Actions 設定。今天的版本只是讓你看到「commit 前可以自動做這些事」的設計概念。記得:好的工具鏈是「讓你寫對程式比寫錯更容易」,不是「等你錯了再來處罰」。
小結
今天把整個開發環境一次建好:uv 統一管理 Python 版本、虛擬環境、套件與 lock 檔;VS Code 搭配 Python、Ruff、Even Better TOML 等擴充套件提供編輯體驗;Git 守住版本歷史;pyproject.toml 與標準目錄結構讓專案可以被重現與分享。我們建立了一個 hello-api 範例專案,包含三個端點、三支煙霧測試、第一次 commit,是後續四十三篇都會沿用的標準結構。
結語
今天把地基打好了,明天,我們會進入 HTTP 與 REST 的世界。你會學到 HTTP 方法、狀態碼、標頭的意義,REST 風格的 API 由哪些來、為什麼業界普遍採用 REST,以及怎麼把昨天的 FastAPI 範例改寫得更符合 REST 慣例。這些觀念在後續每一天都會用到,所以請預留一些時間把觀念弄清楚,別急著抄程式碼。
延伸資源
- uv 官方教學(0.7,2025):
https://docs.astral.sh/uv/ - PEP 621(pyproject.toml 標準):
https://peps.python.org/pep-0621/ - VS Code Python 教學(2025):
https://code.visualstudio.com/docs/languages/python - Pro Git 中文版(2024):
https://git-scm.com/book/zh-tw/v2 - Hatchling build backend 介紹(2025):
https://hatch.pypa.io/latest/ - httpx 官方教學(0.28,2025):
https://www.python-httpx.org/
留言
張貼留言