本文 AI 產出,尚未審核

Python 課程 – 封裝與發佈 (Packaging & Distribution)

主題:版本號規範(Semantic Versioning)


簡介

在 Python 生態系統裡,套件的 版本號 不只是給使用者看「這是第幾版」那麼簡單。它同時承載了 相容性、變更範圍與升級風險 的資訊。若版本號的寫法不一致,開發者在升級相依套件時很容易遇到相容性破壞、測試失敗,甚至是執行時錯誤。

Semantic Versioning(語意化版本號,簡稱 SemVer) 為業界廣泛採用的標準,提供一套明確且可機器判讀的規則。了解並正確使用 SemVer,能讓套件作者在發佈新功能或修正 bug 時,清楚傳達變更的影響;同時也讓使用者在決定是否升級時,有可靠的依據。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握在 Python 專案中正確使用 semantic versioning 的技巧,適合剛踏入套件開發的初學者,也能為已有經驗的中級開發者提供實務參考。


核心概念

1. SemVer 的基本結構

SemVer 的版本號遵循 MAJOR.MINOR.PATCH(主版號.次版號.修補號)三段式,並允許在最後加上 pre‑releasebuild 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 # 含建置資訊的正式版,+ 後面的字串不影響版本排序

重點:只有 MAJORMINORPATCH 會影響版本排序,pre‑release 在同等主版號下會被視為「較舊」的版本,build metadata 則完全不參與比較。


2. 為什麼 Python 專案需要遵守 SemVer?

  1. 相依管理pippoetryconda 等套件管理工具會根據版本號決定是否滿足相依條件。若版本號不符合 SemVer,工具可能錯誤地升級或降級套件,導致相容性問題。
  2. 自動化部署:CI/CD pipeline 常會根據 git tagpyproject.toml 中的版本號自動產生發佈檔案。統一的版本規則讓腳本更簡潔、可靠。
  3. 使用者信任:開源社群已習慣 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. 程式碼範例:自動遞增版本號

以下示範三種常見的自動遞增策略,分別適用於 PATCHMINORMAJOR 的升級情境。程式碼採用純 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.txtpyproject.tomlsetup.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.0a12.0.0b22.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_requiresrequires-python,確保相依範圍正確。
在 PATCH 中加入破壞性變更 依賴此套件的應用程式意外崩潰,信任機制被破壞。 嚴格遵守「PATCH 只能是向下相容」的原則,若有破壞性變更,立即升為 MINORMAJOR
使用 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

最佳實踐總結

  1. 嚴格遵守 MAJOR/MINOR/PATCH 的意義,不要把破壞性變更藏在 PATCH。
  2. 自動化版本遞增(如上範例)結合 CI,避免手動錯誤。
  3. 在發佈前執行測試套件,確保所有公開 API 在新版本仍保持相容。
  4. 使用 pyproject.toml + PEP 621,讓工具(pip, build, twine)一致讀取版本資訊。
  5. 在 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 publish1.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 的重大改寫。為了讓現有使用者有時間遷移,作者採取以下步驟:

  1. main 分支 仍維持 1.5.2(最後的穩定版)。
  2. dev-2.0 分支,每次提交都使用 2.0.0-alpha.N2.0.0-rc.N 作為版本號,並在 pyproject.toml 中設定 prerelease = true
  3. 發布至 TestPyPItwine upload --repository testpypi dist/*,讓早期測試者安裝 pip install -i https://test.pypi.org/simple/ api-client==2.0.0-alpha.1
  4. 正式版發布:當所有測試通過後,刪除 pre‑release 標籤,改為 2.0.0,並 同時README 中加入升級指南(列出破壞性變更)。

這種 分階段發布 的策略,讓開源社群能在安全的環境下先行體驗新功能,同時不干擾穩定版使用者。


總結

  • Semantic Versioning 為 Python 套件提供了清晰、可預測的版本管理規則。
  • 正確的 MAJOR / MINOR / PATCH 劃分,能讓使用者在升級時有明確的風險評估。
  • setup.pypyproject.toml 中宣告版本號,配合 相依範圍>=1.0.0,<2.0.0)可確保自動化工具正確解析。
  • 自動化遞增腳本、CI/CD 整合與 pre‑release 流程,是避免手動錯誤、提升發布品質的關鍵。
  • 了解常見陷阱(如在 PATCH 中加入破壞性變更、忘記移除 pre‑release 標籤)並遵循最佳實踐,能顯著降低相容性問題。

透過本文的概念說明與實作範例,你現在應該能在自己的 Python 專案中 正確、有效率地使用 semantic versioning,不論是內部工具還是公開套件,都能在升級與維護的過程中保持穩定與可預測。祝你在封裝與發佈的旅程中順利前行!