Python 課程 – 封裝與發佈 (Packaging & Distribution)
主題:版本號規範(Semantic Versioning)
簡介
在 Python 生態系統裡,套件的 版本號 不只是給使用者看「這是第幾版」那麼簡單。它同時承載了 相容性、變更範圍與升級風險 的資訊。若版本號的寫法不一致,開發者在升級相依套件時很容易遇到相容性破壞、測試失敗,甚至是執行時錯誤。
Semantic Versioning(語意化版本號,簡稱 SemVer) 為業界廣泛採用的標準,提供一套明確且可機器判讀的規則。了解並正確使用 SemVer,能讓套件作者在發佈新功能或修正 bug 時,清楚傳達變更的影響;同時也讓使用者在決定是否升級時,有可靠的依據。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握在 Python 專案中正確使用 semantic versioning 的技巧,適合剛踏入套件開發的初學者,也能為已有經驗的中級開發者提供實務參考。
核心概念
1. SemVer 的基本結構
SemVer 的版本號遵循 MAJOR.MINOR.PATCH(主版號.次版號.修補號)三段式,並允許在最後加上 pre‑release 與 build metadata。其定義如下:
| 欄位 | 目的 | 變更時機 |
|---|---|---|
| MAJOR | 破壞相容性的變更 | 當 API、行為或依賴關係的變更會導致舊版程式碼無法正常運作時 |
| MINOR | 向下相容的新功能 | 當加入新功能但不影響既有 API 時 |
| PATCH | 向下相容的錯誤修正 | 修復 bug、提升效能或安全性,且不改變任何公開 API |
| pre‑release(可選) | 尚未正式發布的測試版 | 如 alpha, beta, rc(release candidate)等 |
| build metadata(可選) | 與版本號無關的建置資訊 | 如 Git commit SHA、編譯時間等,供 CI/CD 使用 |
範例:
1.4.2 # 正式版,主版號 1、次版號 4、修補號 2
2.0.0-alpha.3 # 前測版,主版號 2、次版號 0、修補號 0,pre‑release 為 alpha.3
3.1.0+20231120 # 含建置資訊的正式版,+ 後面的字串不影響版本排序
重點:只有 MAJOR、MINOR、PATCH 會影響版本排序,pre‑release 在同等主版號下會被視為「較舊」的版本,build metadata 則完全不參與比較。
2. 為什麼 Python 專案需要遵守 SemVer?
- 相依管理:
pip、poetry、conda等套件管理工具會根據版本號決定是否滿足相依條件。若版本號不符合 SemVer,工具可能錯誤地升級或降級套件,導致相容性問題。 - 自動化部署:CI/CD pipeline 常會根據
git tag或pyproject.toml中的版本號自動產生發佈檔案。統一的版本規則讓腳本更簡潔、可靠。 - 使用者信任:開源社群已習慣 SemVer,看到
2.3.0能立刻判斷此版本不會破壞舊有程式碼;若看到2.3.0-beta,則知道仍在測試階段。
3. 在 Python 中宣告版本號
3.1 setup.py(舊式寫法)
# setup.py
from setuptools import setup, find_packages
setup(
name="my-awesome-lib",
version="1.2.0", # <-- 這裡遵循 MAJOR.MINOR.PATCH
packages=find_packages(),
description="A demo library for SemVer illustration",
python_requires=">=3.8",
# 其他 meta 資訊…
)
3.2 pyproject.toml(PEP 621 推薦寫法)
# pyproject.toml
[project]
name = "my-awesome-lib"
version = "1.2.0" # 同樣遵守 SemVer
description = "A demo library for SemVer illustration"
requires-python = ">=3.8"
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
提示:若專案使用
poetry,只要在pyproject.toml裡設定version欄位即可,poetry version命令會自動處理版本遞增。
4. 程式碼範例:自動遞增版本號
以下示範三種常見的自動遞增策略,分別適用於 PATCH、MINOR、MAJOR 的升級情境。程式碼採用純 Python,並以 toml 套件讀寫 pyproject.toml。
# version_bump.py
import toml
import re
from pathlib import Path
PROJECT_FILE = Path("pyproject.toml")
SEMVER_RE = re.compile(r"^(?P<major>0|[1-9]\d*)\."
r"(?P<minor>0|[1-9]\d*)\."
r"(?P<patch>0|[1-9]\d*)"
r"(?:-(?P<pre>[\w\.]+))?"
r"(?:\+(?P<build>[\w\.]+))?$")
def load_version() -> str:
data = toml.load(PROJECT_FILE)
return data["project"]["version"]
def save_version(new_version: str):
data = toml.load(PROJECT_FILE)
data["project"]["version"] = new_version
toml.dump(data, PROJECT_FILE)
def bump(version: str, part: str) -> str:
"""依據 part ('major'|'minor'|'patch') 回傳遞增後的版本字串"""
m = SEMVER_RE.match(version)
if not m:
raise ValueError(f"Invalid SemVer string: {version}")
major, minor, patch = map(int, (m.group("major"), m.group("minor"), m.group("patch")))
pre, build = m.group("pre"), m.group("build")
if part == "major":
major += 1
minor = 0
patch = 0
pre = build = None
elif part == "minor":
minor += 1
patch = 0
pre = build = None
elif part == "patch":
patch += 1
pre = build = None
else:
raise ValueError("part must be 'major', 'minor' or 'patch'")
new_version = f"{major}.{minor}.{patch}"
if pre:
new_version += f"-{pre}"
if build:
new_version += f"+{build}"
return new_version
if __name__ == "__main__":
import sys
if len(sys.argv) != 2 or sys.argv[1] not in {"major", "minor", "patch"}:
print("Usage: python version_bump.py [major|minor|patch]")
sys.exit(1)
current = load_version()
new = bump(current, sys.argv[1])
save_version(new)
print(f"Bumped version: {current} → {new}")
這段腳本示範了 自動化 的版本遞增流程,適合放在 CI 中的「發布前」步驟。
- PATCH:只改變最後一位,適用於 bug 修正。
- MINOR:次版號遞增,常用於新增向下相容功能。
- MAJOR:主版號遞增,表示有破壞相容性的變更。
5. 版本範圍與相依宣告
在 requirements.txt、pyproject.toml 或 setup.cfg 中,我們可以使用 版本範圍 來限制套件相依的允許範圍。以下列出幾種常見寫法,並以 SemVer 為基礎說明其意義。
| 範例 | 含意 |
|---|---|
my-awesome-lib>=1.2.0,<2.0.0 |
允許 1.x 系列的所有向下相容更新(PATCH、MINOR),但不接受 2.0.0 之後的破壞性升級。 |
my-awesome-lib~=1.2.3 |
等同於 >=1.2.3,<1.3.0,只允許同一次版號內的修正。 |
my-awesome-lib==1.2.* |
任意 1.2.x 版本,常用於「我只信任 1.2 系列」的情況。 |
my-awesome-lib>=2.0.0a1,<2.0.0 |
包含 2.0.0a1、2.0.0b2、2.0.0rc1 等 pre‑release,但不包含正式版 2.0.0。 |
最佳實踐:在發布庫時,盡量使用
>=MAJOR.MINOR.0,<MAJOR+1.0.0的範圍,讓使用者在不破壞相容性的前提下,自動取得安全修補與新功能。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方式 / 最佳實踐 |
|---|---|---|
| 忘記在 pre‑release 後移除標籤 | 發布 1.0.0-beta 後直接升級到 1.0.0,但 CI 仍把 beta 當作最新版本,使用者安裝到測試版。 |
在正式版發布前,務必 移除 -beta、-rc 標籤,或使用 poetry version 自動切換。 |
| MAJOR 升版卻未更新相依宣告 | 使用者仍依賴舊版相依,安裝時產生衝突或程式在執行時拋出 ImportError。 |
升版時檢查 install_requires、requires-python,確保相依範圍正確。 |
| 在 PATCH 中加入破壞性變更 | 依賴此套件的應用程式意外崩潰,信任機制被破壞。 | 嚴格遵守「PATCH 只能是向下相容」的原則,若有破壞性變更,立即升為 MINOR 或 MAJOR。 |
使用 0.x.y 作為正式版 |
0 版號在 SemVer 中表示「尚未穩定」,許多工具會將 0.x 視為不穩定,限制升級。 |
在 API 已穩定並通過完整測試後,直接升到 1.0.0,讓使用者放心升級。 |
| 忽略 build metadata | CI/CD 無法根據 commit SHA 或 build 編號追蹤發佈來源。 | 於 pyproject.toml 中加入 +<gitsha>,或在 GitHub Release 標籤中使用 v1.2.3+gabcdef0。 |
最佳實踐總結:
- 嚴格遵守 MAJOR/MINOR/PATCH 的意義,不要把破壞性變更藏在 PATCH。
- 自動化版本遞增(如上範例)結合 CI,避免手動錯誤。
- 在發佈前執行測試套件,確保所有公開 API 在新版本仍保持相容。
- 使用
pyproject.toml+ PEP 621,讓工具(pip,build,twine)一致讀取版本資訊。 - 在 README 或文件中說明相容策略,讓使用者清楚知道升級風險。
實際應用場景
案例 1:內部工具庫的循環升級
公司內部有一套共用的 data-utils 套件,提供資料清洗與轉換功能。開發團隊採用以下流程:
| 步驟 | 說明 |
|---|---|
| 1. 開發新功能 | 在 feature/ 分支上實作 MINOR 變更(如新增 clean_missing()) |
| 2. CI 測試 | 透過 GitHub Actions 執行單元測試,測試全部 >=1.0.0,<2.0.0 的相依套件 |
| 3. 自動遞增 | 合併至 main 後,poetry version minor 產生 1.3.0,自動打 tag v1.3.0 |
| 4. 發佈 | poetry publish 把 1.3.0 上傳至私有 PyPI,並在 requirements.txt 中宣告 data-utils>=1.3.0,<2.0.0 |
| 5. 客戶端升級 | 使用者執行 pip install -U data-utils,只會取得 1.3.x 系列,不會意外升到 2.0.0(破壞相容) |
透過 SemVer + CI/CD,團隊能安全、快速地將新功能推給所有使用者。
案例 2:公開套件的 pre‑release 發佈
一個開源的 API 客戶端 api-client 正在開發 2.0.0 的重大改寫。為了讓現有使用者有時間遷移,作者採取以下步驟:
- 在
main分支 仍維持1.5.2(最後的穩定版)。 - 在
dev-2.0分支,每次提交都使用2.0.0-alpha.N或2.0.0-rc.N作為版本號,並在pyproject.toml中設定prerelease = true。 - 發布至 TestPyPI:
twine upload --repository testpypi dist/*,讓早期測試者安裝pip install -i https://test.pypi.org/simple/ api-client==2.0.0-alpha.1。 - 正式版發布:當所有測試通過後,刪除 pre‑release 標籤,改為
2.0.0,並 同時 在README中加入升級指南(列出破壞性變更)。
這種 分階段發布 的策略,讓開源社群能在安全的環境下先行體驗新功能,同時不干擾穩定版使用者。
總結
- Semantic Versioning 為 Python 套件提供了清晰、可預測的版本管理規則。
- 正確的 MAJOR / MINOR / PATCH 劃分,能讓使用者在升級時有明確的風險評估。
- 在
setup.py、pyproject.toml中宣告版本號,配合 相依範圍(>=1.0.0,<2.0.0)可確保自動化工具正確解析。 - 自動化遞增腳本、CI/CD 整合與 pre‑release 流程,是避免手動錯誤、提升發布品質的關鍵。
- 了解常見陷阱(如在 PATCH 中加入破壞性變更、忘記移除 pre‑release 標籤)並遵循最佳實踐,能顯著降低相容性問題。
透過本文的概念說明與實作範例,你現在應該能在自己的 Python 專案中 正確、有效率地使用 semantic versioning,不論是內部工具還是公開套件,都能在升級與維護的過程中保持穩定與可預測。祝你在封裝與發佈的旅程中順利前行!