uv でのライブラリ公開 — pyproject.toml と PyPI リリース
pyproject.toml の意味を一度に整理し、uv build・uv publish で最初のライブラリを PyPI にリリースする手順を扱います。
第7章 モジュールと pyproject.toml で見た pyproject.toml は「自分のプロジェクトを実行するための設定」という観点でした。本章は同じファイルを 「自分のライブラリを他人が使えるよう PyPI にリリースするための設定」 という観点であらためて見ます。そしてその流れを uv ひとつのツールで完結させます。
第33章 CLI ツールの作成 で作る CLI を、本章の流れで PyPI にリリースできます — 2 つの章で一つのセットです。
なぜ自分でリリースしてみるのか #
ライブラリをリリースする機会がなさそうに見えても、自分で一度やってみると他の PyPI パッケージが なぜその構造になっているのか が見えてきます。依存関係をどう宣言するか、README が PyPI ページにどう表示されるか、wheel と sdist が何で、なぜ両方アップロードするか — こうしたことはすべて、自分でやってみて初めて自然に頭に残ります。
pyproject.toml の標準 — 何がどこに行くのか #
pyproject.toml は複数の PEP が積み重なった結果です。
| セクション | PEP | 役割 |
|---|---|---|
[build-system] | PEP 518 | パッケージをビルドするときどのツールが必要か |
[project] | PEP 621 | パッケージのメタデータ(名前、バージョン、著者、依存関係) |
[project.scripts] | PEP 621 | CLI エントリポイント |
[project.optional-dependencies] | PEP 621 | オプション依存グループ |
[dependency-groups] | PEP 735 | 開発用依存グループ(uv / pip がサポート) |
[tool.<name>] | ツール自由 | 各ツールの設定(ruff、pyright、pytest など) |
最小例 #
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "swcli"
version = "0.1.0"
description = "Schoolofweb sample CLI"
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
authors = [
{ name = "Your Name", email = "you@example.com" },
]
keywords = ["cli", "example"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"typer>=0.12",
"rich>=13",
]
[project.optional-dependencies]
yaml = ["pyyaml>=6"]
[project.urls]
Homepage = "https://github.com/you/swcli"
Issues = "https://github.com/you/swcli/issues"
[project.scripts]
swcli = "swcli.cli:app"
[dependency-groups]
dev = [
"pytest>=8",
"pytest-cov",
"ruff",
"pyright",
]主要フィールドの意味 #
name— PyPI 上の固有名。一度登録されると似た名前もブロックされます(typosquatting 防止)。version— SemVer 推奨(MAJOR.MINOR.PATCH)。requires-python— どの Python バージョンから動作するか。PyPI が互換性のない wheel を自動でフィルタリング。dependencies— 実行時に必須の依存関係。範囲指定 が重要(次の節で)。optional-dependencies— ユーザーがpip install swcli[yaml]のように追加で受け取れる束。dependency-groups.dev— 開発者だけが使う依存関係。配布 wheel には含まれません。project.scripts— インストールするとswcliというコマンドが PATH に作られる。右辺は<module>:<callable>。
src layout vs flat layout #
ライブラリのディレクトリ構造には 2 つの慣習があります。
flat layout
myproject/
pyproject.toml
swcli/
__init__.py
cli.py
tests/src layout
myproject/
pyproject.toml
src/
swcli/
__init__.py
cli.py
tests/src layout が 推奨 です。理由は以下のとおりです。
cd myproject; python -c "import swcli"が偶然動いてしまわないようになります — インストールしなければ import できません。だから「インストールされていない状態でもたまたま動くバグ」が生じません。- テストは常に「インストールされたパッケージ」を import するので、ユーザーの体験と一致します。
src layout を使うと、ビルドバックエンド(hatchling など)が自動的に src/ を探してくれます。
依存関係の宣言 — 範囲指定の原則 #
dependencies に requests とだけ書くと「どんな requests バージョンでも OK」になります。すると 1 年後、ユーザーがインストールしたときに非互換の新しいバージョンが入って壊れることがあります。
ライブラリ と アプリケーション ではポリシーが違います。
- ライブラリ(PyPI 配布対象) — 下限と上限 を明示。
requests>=2.31,<3。上限はメジャーバージョン。 - アプリケーション(uv.lock で固定配布) — uv.lock が正確なバージョンを固定するので、範囲はもっと寛容でも OK。
本章はライブラリ視点なので、次の形式を推奨します。
dependencies = [
"typer>=0.12,<1",
"rich>=13,<15",
]上限がないと、依存関係のメジャーアップデートがサイレントに入ってきてユーザー環境を壊す可能性があります。
uv build — wheel と sdist の作成 #
uv builddist/ フォルダに 2 つのファイルが作られます。
dist/
swcli-0.1.0-py3-none-any.whl
swcli-0.1.0.tar.gz| 形式 | 正体 |
|---|---|
wheel (.whl) | ビルド済みのバイナリ形式。インストールが速い。名前に互換性タグ(py3-none-any)が入る |
sdist (.tar.gz) | ソース配布。ビルド前の状態。ユーザー環境でビルド可能 |
純粋な Python ライブラリは両方アップロードするのが標準です。C 拡張があるパッケージはプラットフォーム別の wheel を複数(manylinux_2_28_x86_64、macosx_11_0_arm64 など)作ってアップロードします。
TestPyPI でリハーサル #
初リリースは TestPyPI に先にアップロードします — ミスしても本番 PyPI が汚れません。
アカウントとトークン #
- https://test.pypi.org/account/register/ で登録。
- https://test.pypi.org/manage/account/token/ で API トークンを発行。名前は単一プロジェクト に絞るのが推奨です(全体権限トークンを環境変数に置かないでください)。
環境変数で渡す #
export UV_PUBLISH_TOKEN="pypi-AgENd..."
export UV_PUBLISH_URL="https://test.pypi.org/legacy/"
uv build
uv publish成功すれば https://test.pypi.org/project/swcli/ で確認できます。
インストールテスト #
uv pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
swcli--extra-index-url を追加する理由は、TestPyPI に依存パッケージがない場合があるので、本番 PyPI から取得するためです。
uv publish — 本番 PyPI へのリリース手順 #
リハーサルが終わったら本番 PyPI へ。
unset UV_PUBLISH_URL # 本番 PyPI がデフォルト
export UV_PUBLISH_TOKEN="pypi-AgEN..." # pypi.org のトークン
uv build
uv publishhttps://pypi.org/project/swcli/ で確認。
インストール:
uv pip install swcli
swcli --helpおめでとうございます — ライブラリが世に出ました。
一度アップしたバージョンは消せない #
PyPI の最も重要な原則です。同じバージョンを再アップロードするのは 拒否 され、間違ってアップロードしたバージョンは 削除はできるものの、その場所に同じバージョンを再アップロードできません。ユーザーの依存関係 resolver が一貫性を保つためです。
ミスしたらバージョンを上げて(0.1.0 → 0.1.1)新しくリリースします。だからこそ TestPyPI のリハーサルが重要 です。
SemVer と CHANGELOG #
Semantic Versioning #
MAJOR.MINOR.PATCH の意味は次のとおりです。
| 変更種別 | 上がる桁 |
|---|---|
| 破壊的変更(signature 変更、削除) | MAJOR |
| 互換性のある新機能追加 | MINOR |
| バグ修正(挙動は同じ) | PATCH |
0.x は「まだ安定 API ではない」という合意です。この区間では MINOR が実質 MAJOR のように破壊的変更を含むことがあります。1.0.0 を打ったら、そこからは上のルールを厳密に守ります。
CHANGELOG.md #
Keep a Changelog 形式が標準です。
# Changelog
## [Unreleased]
## [0.2.0] - 2026-06-01
### Added
- `--json` 出力オプションを追加。
### Fixed
- Windows でカラーコードがそのまま出力されていた問題。
## [0.1.0] - 2026-05-17
- 初リリース。PR をマージするたびに [Unreleased] に 1 行追記し、リリース直前にバージョンセクションへ移す流れです。
README・LICENSE・メタデータ #
PyPI のパッケージページには次のものが表示されます。
- README.md — ページの本文。最初の画面なので、5 秒以内に「何か」+「なぜ使うか」+「どう始めるか」が見えなければなりません。
- License —
[project] license = "MIT"(SPDX 識別子)。なければ人々は使えません(デフォルトは使用禁止)。 - classifiers — PyPI の分類タグ。本書のカテゴリ/トピック/Python バージョンが検索に影響します。
- Homepage / Repository / Issues URL — サイドバーのリンク。
README に バッジ(badge) を付けるのも信頼の合図です。


GitHub Actions でタグ push 自動デプロイ #
毎回手で uv build && uv publish するのはミスの温床です。Git タグを push すれば自動デプロイ されるようにします。
Trusted Publishing — トークン不要の方式 #
PyPI の新しい推奨方式です。GitHub Actions の OIDC トークンで PyPI に認証します。API トークンを secrets に保存する必要がないので最も安全 です。
PyPI 側の設定:
https://pypi.org/manage/project/<name>/settings/publishing/に移動。- 「Add a new publisher」→ GitHub を選択。
- owner、repository、workflow filename、environment name を入力。
ワークフローファイル:
name: Publish to PyPI
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv python install 3.14
- run: uv build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi # PyPI 側の設定と一致
permissions:
id-token: write # OIDC トークン発行に必要
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1タグの push:
git tag v0.2.0
git push origin v0.2.0GitHub Actions がビルド → OIDC で PyPI 認証 → publish まで自動で処理します。
よくある落とし穴 #
同じバージョンを再アップロードしようとする #
すでに上の節で見たとおり。PyPI は拒否します。バージョンを上げて新しくリリースします。
依存関係の上限が欠落 #
dependencies = ["pydantic"] # 非推奨
dependencies = ["pydantic>=2,<3"] # 推奨上限がないと、依存関係のメジャーアップデートがユーザー環境をサイレントに壊す可能性があります。
LICENSE 欠落 #
license が空、または誤った識別子だと、PyPI が警告したり、会社のポリシーに従って社内利用がブロックされたりします。可能な限り SPDX 識別子(MIT、Apache-2.0、BSD-3-Clause など)を使います。
大きなファイルを sdist に含める #
.gitignore と tool.hatch.build.targets.sdist.exclude は別物です。ビルド結果を一度開いて(unzip dist/*.whl -d /tmp/inspect; ls -lh /tmp/inspect)確認するのが安全です。
CLI エントリポイントの間違ったパス #
project.scripts の右辺は モジュールパス です。
swcli = "swcli.cli:app" # OK — src/swcli/cli.py の app
swcli = "src.swcli.cli:app" # 誤り — src はパッケージではないインストール後に swcli --help が「module not found」で失敗したら、このパスを疑います。
TestPyPI と本番 PyPI のトークンを取り違える #
ドメインが違いますし、トークンも違います。自動化では環境ごとに secret を分けて持ちます(PYPI_TOKEN、TEST_PYPI_TOKEN)。
第33章とのセット — Typer CLI のリリース #
第33章 で作る Typer ベースの CLI を、本章の流れでそのままリリースできます。
swcli/
pyproject.toml ← 本章
src/swcli/
__init__.py
cli.py ← 第33章
tests/
README.md
CHANGELOG.md
.github/workflows/
ci.yml ← 第30章
publish.yml ← 本章第30章の CI を通過したコードだけが main に入り、main にタグが push されると自動で PyPI にリリースされる構成です。
練習問題 #
- wheel の中を覗く — 本章の流れで作った
dist/<name>-0.1.0-py3-none-any.whlをunzipで展開してみてください。何が入っていますか?METADATAファイルを開いて、自分が pyproject.toml に書いた内容がそのまま入っているか確認し、抜けている項目(例: classifier の typo)がないか確認してください。 - 依存関係範囲の影響 — pyproject.toml のある依存関係を
pydantic>=2(上限なし)に変えてビルドした wheel をインストールした環境で、uv add pydantic==3.0.0a1(仮想新バージョン)のような prerelease を取得してみてください。resolver はどう動作しますか? 上限<3を再度追加すると結果はどう変わりますか? - Trusted Publishing 設定 — 自身の GitHub repo と PyPI アカウントに Trusted Publishing を直接設定し、v0.0.1 を一度リリースしてみてください。
permissions: id-token: writeが抜けるとどんなエラーが出ますか?environment: pypiが PyPI の設定と違うとどんなエラーが出ますか?
次の章は CLI ツールの作成 (Typer) です。本章の流れでリリースするそのパッケージを実際に作る段階です。