一、前言 #
嗨,我是拍拍君 📦
你有沒有寫過一個超好用的工具函式,然後在每個專案都手動複製那份 .py 檔?或者同事問你「欸那個 utils 怎麼用」,你說「去那個 repo 的 src 資料夾裡面找」?
這就是 Python 套件打包 能幫你解決的問題。把你的程式碼打包成一個套件,就可以用 pip install 安裝,跟你平常裝 requests、numpy 一樣方便。
而且!從 2024 年開始,Python 社群已經全面擁抱 pyproject.toml 作為唯一的設定檔。再也不用糾結 setup.py、setup.cfg、requirements.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 工具。格式是 指令名 = "模組路徑:函式名"。
如果你用過 click 或 typer,這個入口點就是把你的 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
以前要分散在 .flake8、pytest.ini、mypy.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 .
-e 是 editable 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/*
第一次上傳需要:
- 到 pypi.org 註冊帳號
- 建立 API token(Settings → API tokens)
- 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.toml、src/ 目錄、.python-version 等等。省去很多手動建檔的功夫。
而且 uv build 和 uv publish 速度比 python -m build + twine 快好幾倍。如果你還沒用 uv,可以回去看拍拍君的 uv 教學!
八、常見問題 FAQ #
Q1:setup.py 還需要嗎?
#
基本上不需要了。 除非你有 C extension 需要特殊的建構流程,否則 pyproject.toml 完全能取代 setup.py 和 setup.cfg。
Q2:requirements.txt 還要留嗎?
#
看情況:
- 套件(library):不需要,依賴寫在
pyproject.toml的dependencies裡 - 應用程式(application):可以用
uv lock或pip 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 套件打包!📦
回顧一下重點:
pyproject.toml是唯一需要的設定檔——忘了setup.py吧- src layout 是官方推薦的目錄結構
- hatchling 是新專案的好選擇
- TestPyPI 先測試再發佈,避免尷尬
- Trusted Publisher + GitHub Actions 讓發佈完全自動化
- uv 可以加速整個流程
下次寫了一個好用的工具,別再 cp 到處複製了——打包上 PyPI,讓全世界都能 pip install 你的作品吧!
拍拍君相信你可以的 💪