目次
5部 運用・パッケージング・テスト
  1. 30.型チェッカ設定と CI 統合
  2. 31.logging と観測性
  3. 32.uv でのライブラリ公開 — pyproject.toml と PyPI リリース
  4. 33.CLI ツールの作成 (Typer)
32 章

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 621CLI エントリポイント
[project.optional-dependencies]PEP 621オプション依存グループ
[dependency-groups]PEP 735開発用依存グループ(uv / pip がサポート)
[tool.<name>]ツール自由各ツールの設定(ruff、pyright、pytest など)

最小例 #

pyproject.toml — ライブラリ用の最小形
[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/ を探してくれます。

依存関係の宣言 — 範囲指定の原則 #

dependenciesrequests とだけ書くと「どんな 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 build

dist/ フォルダに 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_64macosx_11_0_arm64 など)作ってアップロードします。

TestPyPI でリハーサル #

初リリースは TestPyPI に先にアップロードします — ミスしても本番 PyPI が汚れません。

アカウントとトークン #

  1. https://test.pypi.org/account/register/ で登録。
  2. 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 publish

https://pypi.org/project/swcli/ で確認。

インストール:

uv pip install swcli
swcli --help

おめでとうございます — ライブラリが世に出ました。

一度アップしたバージョンは消せない #

PyPI の最も重要な原則です。同じバージョンを再アップロードするのは 拒否 され、間違ってアップロードしたバージョンは 削除はできるものの、その場所に同じバージョンを再アップロードできません。ユーザーの依存関係 resolver が一貫性を保つためです。

ミスしたらバージョンを上げて(0.1.00.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.md
# 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) を付けるのも信頼の合図です。

![CI](https://github.com/you/swcli/actions/workflows/ci.yml/badge.svg)
![PyPI](https://img.shields.io/pypi/v/swcli)
![Python](https://img.shields.io/pypi/pyversions/swcli)

GitHub Actions でタグ push 自動デプロイ #

毎回手で uv build && uv publish するのはミスの温床です。Git タグを push すれば自動デプロイ されるようにします。

Trusted Publishing — トークン不要の方式 #

PyPI の新しい推奨方式です。GitHub Actions の OIDC トークンで PyPI に認証します。API トークンを secrets に保存する必要がないので最も安全 です。

PyPI 側の設定:

  1. https://pypi.org/manage/project/<name>/settings/publishing/ に移動。
  2. 「Add a new publisher」→ GitHub を選択。
  3. owner、repository、workflow filename、environment name を入力。

ワークフローファイル:

.github/workflows/publish.yml
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.0

GitHub Actions がビルド → OIDC で PyPI 認証 → publish まで自動で処理します。

よくある落とし穴 #

同じバージョンを再アップロードしようとする #

すでに上の節で見たとおり。PyPI は拒否します。バージョンを上げて新しくリリースします。

依存関係の上限が欠落 #

dependencies = ["pydantic"]   # 非推奨
dependencies = ["pydantic>=2,<3"]   # 推奨

上限がないと、依存関係のメジャーアップデートがユーザー環境をサイレントに壊す可能性があります。

LICENSE 欠落 #

license が空、または誤った識別子だと、PyPI が警告したり、会社のポリシーに従って社内利用がブロックされたりします。可能な限り SPDX 識別子(MITApache-2.0BSD-3-Clause など)を使います。

大きなファイルを sdist に含める #

.gitignoretool.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_TOKENTEST_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 にリリースされる構成です。

練習問題 #

  1. wheel の中を覗く — 本章の流れで作った dist/<name>-0.1.0-py3-none-any.whlunzip で展開してみてください。何が入っていますか? METADATA ファイルを開いて、自分が pyproject.toml に書いた内容がそのまま入っているか確認し、抜けている項目(例: classifier の typo)がないか確認してください。
  2. 依存関係範囲の影響 — pyproject.toml のある依存関係を pydantic>=2(上限なし)に変えてビルドした wheel をインストールした環境で、uv add pydantic==3.0.0a1(仮想新バージョン)のような prerelease を取得してみてください。resolver はどう動作しますか? 上限 <3 を再度追加すると結果はどう変わりますか?
  3. Trusted Publishing 設定 — 自身の GitHub repo と PyPI アカウントに Trusted Publishing を直接設定し、v0.0.1 を一度リリースしてみてください。permissions: id-token: write が抜けるとどんなエラーが出ますか? environment: pypi が PyPI の設定と違うとどんなエラーが出ますか?
注記
一行まとめ — pyproject.toml 一つのファイルにメタデータ・依存関係・CLI エントリポイントをすべて宣言し、uv build で wheel/sdist を作り、TestPyPI でリハーサルしたあと本番 PyPI にリリースします。自動化は GitHub Actions + Trusted Publishing で — トークンを保管しないのが最も安全な答えです。

次の章は CLI ツールの作成 (Typer) です。本章の流れでリリースするそのパッケージを実際に作る段階です。

X