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

Python Packaging:從 pyproject.toml 到發佈 PyPI 全攻略

·7 分鐘· loading · loading · ·
Python Packaging Pyproject.toml Pypi Pip Uv Setuptools
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 39: 本文

一、前言
#

嗨,我是拍拍君 📦

你有沒有寫過一個超好用的工具函式,然後在每個專案都手動複製那份 .py 檔?或者同事問你「欸那個 utils 怎麼用」,你說「去那個 repo 的 src 資料夾裡面找」?

這就是 Python 套件打包 能幫你解決的問題。把你的程式碼打包成一個套件,就可以用 pip install 安裝,跟你平常裝 requests、numpy 一樣方便。

而且!從 2024 年開始,Python 社群已經全面擁抱 pyproject.toml 作為唯一的設定檔。再也不用糾結 setup.pysetup.cfgrequirements.txt 誰管什麼了——一個檔案搞定所有事

今天拍拍君就帶你從零開始,寫一個可以發佈到 PyPI 的 Python 套件!

二、專案結構
#

在開始寫 pyproject.toml 之前,先搞清楚一個 Python 套件的標準目錄長什麼樣子:

my-awesome-tool/
├── pyproject.toml          # 套件設定檔(主角!)
├── README.md               # 專案說明
├── LICENSE                  # 授權條款
├── src/
│   └── awesome_tool/       # 套件原始碼(src layout)
│       ├── __init__.py
│       ├── core.py
│       └── cli.py
└── tests/
    ├── __init__.py
    └── test_core.py

這裡用的是 src layout——把套件放在 src/ 目錄下。為什麼?

  • 避免開發時「以為 import 成功但其實是讀到本地檔案」的陷阱
  • pytest 測試會強制安裝套件後再跑,更接近真實使用情境
  • 這是 Python Packaging User Guide 官方推薦的做法

另一種常見的是 flat layout(直接在根目錄放套件資料夾),對小專案也完全 OK。但如果你在糾結選哪個,選 src layout 準沒錯。

三、pyproject.toml 完全解析
#

pyproject.toml 是整個打包流程的核心。我們一段一段來看:

3.1 建構系統 [build-system]
#

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

這告訴 pip/uv:「用 hatchling 來建構這個套件」。常見的 build backend 有:

Build Backend 特色 適合誰
hatchling 現代、快速、設定簡潔 大多數新專案 ✅
setuptools 老牌、穩定、生態最大 既有專案、需要 C extension
flit-core 極簡、幾乎零設定 純 Python 的小套件
pdm-backend PDM 生態的選擇 PDM 使用者
maturin Rust + Python 混合 PyO3 / Rust extension

拍拍君推薦新專案用 hatchling——設定最簡潔,功能齊全,而且 Hatch 這個工具鏈越來越受歡迎。

3.2 專案元資料 [project]
#

[project]
name = "awesome-tool"
version = "0.1.0"
description = "拍拍君的超讚工具"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
    { name = "拍拍君", email = "pypy@example.com" },
]
keywords = ["cli", "tool", "awesome"]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
]
dependencies = [
    "click>=8.0",
    "rich>=13.0",
]

幾個重點:

  • name:PyPI 上的套件名稱,必須全域唯一。慣例用 - 分隔(但 import 時用 _
  • version:語意化版本號。之後會教你怎麼自動管理
  • requires-python:最低 Python 版本需求
  • dependencies:套件依賴,跟 requirements.txt 格式幾乎一樣

3.3 可選依賴 [project.optional-dependencies]
#

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "ruff>=0.4",
    "mypy>=1.0",
]
docs = [
    "mkdocs>=1.5",
    "mkdocs-material>=9.0",
]

這樣使用者可以選擇要不要安裝額外依賴:

# 只裝核心
pip install awesome-tool

# 裝開發用的工具
pip install awesome-tool[dev]

# 裝文件產生器
pip install awesome-tool[docs]

# 全裝
pip install awesome-tool[dev,docs]

如果你用過 requirements-dev.txt 這種東西,可選依賴就是它的正式替代品。

3.4 CLI 入口點 [project.scripts]
#

[project.scripts]
awesome = "awesome_tool.cli:main"

安裝套件後,使用者就能直接在終端打 awesome 執行你的 CLI 工具。格式是 指令名 = "模組路徑:函式名"

如果你用過 clicktyper,這個入口點就是把你的 CLI 函式接上系統指令的橋樑。

3.5 工具設定 [tool.*]
#

pyproject.toml 還能統一管理其他工具的設定:

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]

[tool.mypy]
python_version = "3.10"
strict = true

以前要分散在 .flake8pytest.inimypy.ini 的設定,現在全部集中到一個檔案。清爽!

四、建構與本地測試
#

4.1 寫點程式碼
#

先來寫我們的套件程式碼。src/awesome_tool/__init__.py

"""拍拍君的超讚工具"""

__version__ = "0.1.0"

src/awesome_tool/core.py

def greet(name: str) -> str:
    """打招呼函式"""
    return f"嗨 {name},歡迎使用拍拍君的超讚工具!🎉"


def add(a: int, b: int) -> int:
    """超強加法器(才不是普通的加法)"""
    return a + b

src/awesome_tool/cli.py

import click
from .core import greet


@click.command()
@click.argument("name", default="世界")
def main(name: str) -> None:
    """拍拍君的超讚工具 CLI"""
    click.echo(greet(name))

4.2 本地安裝測試
#

在專案根目錄執行:

# 用 pip(editable mode,修改程式碼後不用重裝)
pip install -e .

# 或用 uv(更快!)
uv pip install -e .

-eeditable mode——安裝後會建立一個連結指向你的原始碼,改了程式碼不用重新安裝就能生效。開發時必用!

安裝完測試一下:

$ awesome 拍拍醬
嗨 拍拍醬,歡迎使用拍拍君的超讚工具!🎉

4.3 跑測試
#

先寫個簡單的測試 tests/test_core.py

from awesome_tool.core import greet, add


def test_greet():
    result = greet("拍拍君")
    assert "拍拍君" in result
    assert "🎉" in result


def test_add():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0

然後跑 pytest

$ pytest
======================== test session starts ========================
collected 2 items

tests/test_core.py ..                                          [100%]

========================= 2 passed in 0.01s =========================

五、版本管理策略
#

手動改版本號很容易忘記或改錯。這裡介紹兩種自動化方式:

5.1 單一來源版本(推薦)
#

pyproject.toml 裡用動態版本:

[project]
name = "awesome-tool"
dynamic = ["version"]

[tool.hatch.version]
path = "src/awesome_tool/__init__.py"

這樣 hatchling 會自動從 __init__.py 裡的 __version__ 讀取版本號。只要維護一個地方就好!

5.2 用 Git tag 管理版本
#

如果你偏好用 Git tag 來控制版本,可以用 hatch-vcs

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"

這樣 git tag v0.2.0 之後,建構出來的套件就會自動是 0.2.0 版。搭配 CI/CD 特別好用。

六、建構與發佈到 PyPI
#

6.1 建構套件
#

# 安裝建構工具
pip install build

# 建構!
python -m build

建構完會在 dist/ 目錄下產生兩個檔案:

dist/
├── awesome_tool-0.1.0-py3-none-any.whl    # wheel(安裝用)
└── awesome_tool-0.1.0.tar.gz              # sdist(原始碼)
  • wheel (.whl):預先建構好的二進位格式,安裝超快
  • sdist (.tar.gz):原始碼壓縮包,需要在安裝時建構

6.2 先用 TestPyPI 試跑
#

正式發佈之前,先到 TestPyPI 測試一下:

# 安裝上傳工具
pip install twine

# 上傳到 TestPyPI
twine upload --repository testpypi dist/*

# 從 TestPyPI 安裝來測試
pip install --index-url https://test.pypi.org/simple/ awesome-tool

確認沒問題後,再上傳到正式的 PyPI。

6.3 發佈到 PyPI
#

# 上傳到正式 PyPI
twine upload dist/*

第一次上傳需要:

  1. pypi.org 註冊帳號
  2. 建立 API token(Settings → API tokens)
  3. twine 會問你 username 和 password,username 輸入 __token__,password 貼上你的 API token

6.4 用 Trusted Publisher(推薦!)
#

比 API token 更安全的做法是用 Trusted Publisher。只要在 PyPI 設定你的 GitHub repo,GitHub Actions 就能自動發佈,完全不需要管理 token:

# .github/workflows/publish.yml
name: Publish to PyPI

on:
  release:
    types: [published]

permissions:
  id-token: write

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: pypi
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install build
      - run: python -m build
      - uses: pypa/gh-action-pypi-publish@release/v1

設定完之後,在 GitHub 建立 Release 就會自動發佈到 PyPI。超級方便!

七、用 uv 加速整個流程
#

如果你已經在用 uv,整個打包流程可以更快更順:

# 初始化新專案(自動建立 pyproject.toml + src layout)
uv init --lib awesome-tool
cd awesome-tool

# 安裝依賴(飛快)
uv sync

# 開發模式安裝
uv pip install -e .

# 建構
uv build

# 發佈(uv 內建 publish 功能!)
uv publish

uv init --lib 會幫你建立完整的專案結構,包含 pyproject.tomlsrc/ 目錄、.python-version 等等。省去很多手動建檔的功夫。

而且 uv builduv publish 速度比 python -m build + twine 快好幾倍。如果你還沒用 uv,可以回去看拍拍君的 uv 教學

八、常見問題 FAQ
#

Q1:setup.py 還需要嗎?
#

基本上不需要了。 除非你有 C extension 需要特殊的建構流程,否則 pyproject.toml 完全能取代 setup.pysetup.cfg

Q2:requirements.txt 還要留嗎?
#

看情況:

  • 套件(library):不需要,依賴寫在 pyproject.tomldependencies
  • 應用程式(application):可以用 uv lockpip freeze 產生 lock file 來固定版本

Q3:怎麼選 build backend?
#

情境 推薦
新的純 Python 專案 hatchling
既有大型專案 setuptools(遷移成本低)
極簡小套件 flit-core
Rust extension maturin

Q4:src/ layout vs flat layout?
#

src layout flat layout
結構 src/pkg/ pkg/
好處 隔離開發 vs 安裝環境 簡單直覺
壞處 import 路徑多一層 可能誤 import 本地檔案
推薦 ✅ 新專案 小腳本/工具

結語
#

恭喜你學會了 Python 套件打包!📦

回顧一下重點:

  1. pyproject.toml 是唯一需要的設定檔——忘了 setup.py
  2. src layout 是官方推薦的目錄結構
  3. hatchling 是新專案的好選擇
  4. TestPyPI 先測試再發佈,避免尷尬
  5. Trusted Publisher + GitHub Actions 讓發佈完全自動化
  6. uv 可以加速整個流程

下次寫了一個好用的工具,別再 cp 到處複製了——打包上 PyPI,讓全世界都能 pip install 你的作品吧!

拍拍君相信你可以的 💪

延伸閱讀
#

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

相關文章

超快速 Python 套件管理:uv 完全教學
·6 分鐘· loading · loading
Python Uv Package Manager Rust
GitHub Actions 進階:Matrix Build + 自動發佈 PyPI
·5 分鐘· loading · loading
Github-Actions Cicd Pypi Matrix-Build Python
Python Generators/Yield 完全攻略:惰性運算的藝術
·7 分鐘· loading · loading
Python Generator Yield Lazy Evaluation Iterator
Python click:比 argparse 更優雅的 CLI 框架
·10 分鐘· loading · loading
Python Click Cli 命令列 開發工具
Python pytest:fixture + parametrize + mock 完整指南
·8 分鐘· loading · loading
Python Pytest Testing Fixture Mock Parametrize TDD
Python threading:多執行緒並行的正確打開方式
·8 分鐘· loading · loading
Python Threading Concurrency 並行 GIL