結論

PyPIとは、pip install が既定でパッケージを取りに行く、Python の公式のパッケージ置き場です。pyproject.toml を書き、build で作って twine で上げると公開できます。

この記事でわかること

  • 公開までの6手順と、そのまま使える pyproject.toml
  • wheel の中身7ファイルと、アップロードで送られる項目
  • 400・403・README 不具合などエラー原文10種と直し方

2026年10月22日更新|twine 7.0.0・build 1.6.1・pypiserver 2.4.2 で実行確認

自分のコードを PyPI に置くと、世界中の人が pip install 名前 で入れられるようになります。流れは次の6段階です。

flowchart TD
    A[pyproject.toml を書く] --> B[python -m build]
    B --> C[dist に sdist と wheel]
    C --> D[twine upload]
    D --> E[PyPI に公開]
    E --> F[pip install 名前]

急いでいる人向けに、よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
PyPI とは Python の公式パッケージ置き場。読みはパイピーアイ PyPIとは
公開の最短手順 pyproject.toml → build → twine upload 公開する流れ
ビルドのコマンド python -m build(sdist と wheel ができる) ビルド
アップロード twine upload dist/*(ユーザー名は __token__) twine
練習したい TestPyPI に -r testpypi で上げる TestPyPI
CI から公開 Trusted Publishing(トークン不要) Trusted Publishing
400 File already exists 同じ版は上げ直せない。版番号を上げる エラー
README が出ない readme の拡張子と twine check を確かめる エラー

確認環境:Ubuntu 24.04.4・Python 3.11.15・pip 26.2.2・build 1.6.1・twine 7.0.0・hatchling 1.32.4・packaging 26.3。本物の PyPI・TestPyPI の代わりに、手元に pypiserver 2.4.2 でパスワード認証つきのインデックス(127.0.0.1:34400)を立て、サンプルのパッケージ rjc-greet を build → twine check → twine upload → pip install まで実行しました。アップロードで送られる項目は、自作の受け口に twine を向けて確かめました。PyPI の仕様・画面の手順・エラーの文言は docs.pypi.org・pypi.org のヘルプ・Python Packaging User Guide・PyPI のソース(pypi/warehouse)で確認し、PyPI の Simple API の応答は読み取りだけ行いました(確認日 2026年10月22日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PyPIとは|パッケージ公開の手順・pyproject.toml・エラーを実演」

PyPIとは(読み方・pipとの関係・規模)

この節では、PyPI が何で、pip・TestPyPI とどういう関係なのかが分かります。

PyPI(Python Package Index)は、Python のパッケージ(ライブラリやツール)を公開・配布するための公式のリポジトリです。 読み方は「パイピーアイ」で、PyPI のヘルプでも「pie pea eye」と1文字ずつ読むとされています(「パイパイ」と読むと、別の PyPy という Python の実装と紛らわしくなります)。

運営は Python Software Foundation(PSF)で、pip install requests のように名前だけを指定したとき、pip は PyPI からファイルを取ってきます。アカウントを作れば、誰でも無料で自分のパッケージを公開できます。

92.2万PyPI のプロジェクト数2026年10月22日時点の pypi.org の表示
995万リリース(版)の数同 9,954,574 releases
2,224万ファイルの数同 22,239,253 files

似た名前のものとの関係です。

名前 何か 関係
PyPI 公式のパッケージ置き場(pypi.org) 公開先・取得元
pip パッケージを入れるコマンド 既定で PyPI から取る
TestPyPI 練習用の別の PyPI(test.pypi.org) アカウントも別
twine PyPI へアップロードするコマンド 公開に使う
build sdist と wheel を作るコマンド 公開の前に使う
PyPy Python の別の実装(処理系) PyPI とは無関係

pip の使い方そのもの(版の指定・requirements.txt)は、関連記事「pipコマンド一覧|installのバージョン指定とrequirements.txt」で扱っています。

pipはどこからパッケージを入れるのか(Simple APIの仕組み)

この節では、pip install を実行したときに、裏で何の URL にアクセスしているかが分かります。

pip の --index-url のヘルプには、既定値が書かれています。

  -i, --index-url <url>       Base URL of the Python Package Index (default
                              https://pypi.org/simple). This should point to a
                              repository compliant with PEP 503 (the simple
                              repository API) or a local directory laid out in

pip は「Simple API」という決まった形の URL を読みに行きます。 パッケージ名ごとのページ(/simple/パッケージ名/)にファイルの一覧があり、pip は自分の Python で使えるファイルを選んでダウンロードします。

flowchart TD
    P[pip install rjc-greet] --> S[GET /simple/rjc-greet/]
    S --> L[ファイルの一覧<br>版・ハッシュ・対応 Python]
    L --> C[使える wheel を選ぶ]
    C --> D[ファイルを取得<br>ハッシュを確かめる]
    D --> I[site-packages に展開]

手元のインデックスに公開した rjc-greet を、-vv を付けて入れたときの出力の一部です。

Looking in indexes: http://127.0.0.1:34400/simple/
Fetching project page and analyzing links: http://127.0.0.1:34400/simple/rjc-greet/
Getting page http://127.0.0.1:34400/simple/rjc-greet/
http://127.0.0.1:34400 "GET /simple/rjc-greet/ HTTP/1.1" 200 951
Fetched page http://127.0.0.1:34400/simple/rjc-greet/ as text/html; charset=UTF-8
  Found link http://127.0.0.1:34400/packages/rjc_greet-0.1.0-py3-none-any.whl#sha256=6bfe91d3dea70fbc4632ac94d94e46ba520907147c241249ae4876c4993be96d (from http://127.0.0.1:34400/simple/rjc-greet/), version: 0.1.0
  Found link http://127.0.0.1:34400/packages/rjc_greet-0.1.0.tar.gz#sha256=7d27eb33749a16e7c093e60a947788d6f08a514245fe92d3789dbfc1fbfdf508 (from http://127.0.0.1:34400/simple/rjc-greet/), version: 0.1.0
  Found link http://127.0.0.1:34400/packages/rjc_greet-0.1.1-py3-none-any.whl#sha256=658b3787d301d8022e5a660f61e5478596fd7659565667df38a58d7e1476001b (from http://127.0.0.1:34400/simple/rjc-greet/), version: 0.1.1

ページの中身は、ファイルへのリンクが並んだだけの HTML です。リンクの末尾の #sha256=… はファイルのハッシュで、pip はダウンロードしたファイルと照合します。

<h1>Links for rjc-greet</h1>
    <a href="/packages/rjc_greet-0.1.0-py3-none-any.whl#sha256=6bfe91d3dea70fbc4632ac94d94e46ba520907147c241249ae4876c4993be96d">rjc_greet-0.1.0-py3-none-any.whl</a><br>
    <a href="/packages/rjc_greet-0.1.0.tar.gz#sha256=7d27eb33749a16e7c093e60a947788d6f08a514245fe92d3789dbfc1fbfdf508">rjc_greet-0.1.0.tar.gz</a><br>
    <a href="/packages/rjc_greet-0.1.1-py3-none-any.whl#sha256=658b3787d301d8022e5a660f61e5478596fd7659565667df38a58d7e1476001b">rjc_greet-0.1.1-py3-none-any.whl</a><br>
    <a href="/packages/rjc_greet-0.1.1.tar.gz#sha256=9b8eb733154962d36cf022deef464b61809d214928fa5e1dcd1874a1122dc146">rjc_greet-0.1.1.tar.gz</a><br>

本物の PyPI の Simple API は JSON でも返す

PyPI は、Accept: application/vnd.pypi.simple.v1+json を付けると同じ情報を JSON で返します。twine 自身のページを読み取った結果です(api-version 1.4、ファイル 123 個のうちの1つ)。

{
 "core-metadata": {"sha256": "5668eb61a9b49d6869598af191e708c38960d403c1f2d66401817dc2978677e9"},
 "filename": "twine-7.0.0-py3-none-any.whl",
 "hashes": {"sha256": "b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7"},
 "provenance": "https://pypi.org/integrity/twine/7.0.0/twine-7.0.0-py3-none-any.whl/provenance",
 "requires-python": ">=3.10",
 "size": 43204,
 "upload-time": "2026-07-27T15:58:59.260418Z",
 "yanked": false
}

ファイルの実体は https://files.pythonhosted.org/packages/… に置かれています。requires-python は対応する Python の範囲、core-metadata はファイルを丸ごと落とさずに依存関係を読むためのメタデータ、provenance は後で説明する Trusted Publishing で作られた「どこでビルドされたか」の証明です。

pip は requires-python を見て、合わない版を落とす前に飛ばします。 手元の pypiserver はこの情報をページに載せないので、Python 3.9 向けに取ろうとすると、pip は wheel を2つともダウンロードしてから判定しました。

  Downloading rjc_greet-0.1.1-py3-none-any.whl (2.4 kB)
INFO: pip is looking at multiple versions of rjc-greet to determine which version is compatible with other requirements. This could take a while.
  Downloading rjc_greet-0.1.0-py3-none-any.whl (2.4 kB)
ERROR: Packages require a different Python. 3.9.0 not in:
'>=3.10' (required by rjc-greet 0.1.1 (from http://127.0.0.1:34400/packages/rjc_greet-0.1.1-py3-none-any.whl#sha256=658b…))
'>=3.10' (required by rjc-greet 0.1.0 (from http://127.0.0.1:34400/packages/rjc_greet-0.1.0-py3-none-any.whl#sha256=6bfe…))

パッケージ名は「正規化」して扱われる

Simple API の仕様では、名前を小文字にし、.・-・_ の連続を - 1つにしたものが URL になります。仕様に載っている Python の関数で確かめました。

import re
normalize = lambda name: re.sub(r"[-_.]+", "-", name).lower()
for s in ["rjc-greet", "RJC_Greet", "rjc.greet", "Rjc__Greet"]:
    print(f"{s:12} -> {normalize(s)}")
rjc-greet    -> rjc-greet
RJC_Greet    -> rjc-greet
rjc.greet    -> rjc-greet
Rjc__Greet   -> rjc-greet

/simple/RJC_Greet/ にアクセスすると、pypiserver は 301 で /simple/rjc-greet/ に転送しました。pip install RJC_Greet と pip install rjc-greet は同じパッケージを指します。 公開する名前を決めるときは、正規化した形で既存の名前とぶつからないかを見ます。

PyPIにパッケージを公開する流れ(全体の手順)

この節では、初めて公開するときの手順を、上から順にたどれます。

1

アカウントを作る(PyPI と TestPyPI は別)

メールアドレスを確認し、2要素認証を有効にする。PyPI のヘルプでは2要素認証は必須。

初回だけ

2

名前を決めて空いているか調べる

pypi.org/project/名前/ が 404 なら未使用。似すぎた名前は登録できない。

正規化した形で比べる

3

pyproject.toml・README・LICENSE を置く

名前・版・説明・依存・対応 Python を書く。

次の節

4

python -m build でビルド

dist/ に sdist(.tar.gz)と wheel(.whl)ができる。twine check で README を確かめる。

5

TestPyPI に上げて入れてみる

twine upload -r testpypi dist/* のあと、別の仮想環境で pip install。

練習

6

PyPI に上げる

手元からは API トークン、GitHub Actions からは Trusted Publishing を使う。

本番

手元でまとめて実行するコマンドは次のとおりです(Windows は python3 を py に読み替えます)。

python3 -m pip install --upgrade build twine
python3 -m build                                 # dist/ に sdist と wheel
python3 -m twine check dist/*                    # README が PyPI で表示できるか
python3 -m twine upload --repository testpypi dist/*   # 練習
python3 -m twine upload dist/*                   # 本番の PyPI

ビルドとアップロードの道具は、プロジェクトとは別の仮想環境に入れておくと、依存がぶつかりません。仮想環境の作り方は、関連記事「Python venvの使い方|仮想環境の作成・有効化・削除をOS別にコピペ」で扱っています。

プロジェクトのフォルダ構成とpyproject.tomlの書き方

この節では、公開できる最小のプロジェクトの形と、pyproject.toml の各項目の意味が分かります。

この記事で公開したサンプル rjc-greet の構成です。Python Packaging User Guide のチュートリアルと同じ src/ レイアウトにしています。

.
|-- LICENSE
|-- README.md
|-- pyproject.toml
|-- src
|   `-- rjc_greet
|       |-- __init__.py
|       `-- cli.py
`-- tests
    `-- test_greet.py

配布の名前(rjc-greet)と、import する名前(rjc_greet)は別物です。 import 名にハイフンは使えないので、配布名のハイフンをアンダースコアにした名前をフォルダ名にするのが一般的です。

pyproject.toml(そのまま使える完成形)

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

[project]
name = "rjc-greet"
version = "0.1.0"
description = "名前を受け取ってあいさつ文を返す小さなサンプル"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICEN[CS]E*"]
authors = [{ name = "RJC Tech Blog", email = "dev@example.com" }]
keywords = ["sample", "greeting"]
classifiers = [
    "Programming Language :: Python :: 3",
    "Operating System :: OS Independent",
]
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=8"]

[project.scripts]
rjc-greet = "rjc_greet.cli:main"

[project.urls]
Homepage = "https://example.com/rjc-greet"
Issues = "https://example.com/rjc-greet/issues"

src/rjc_greet/__init__.py と cli.py は次のとおりです。

# src/rjc_greet/__init__.py
"""あいさつ文を作る小さなパッケージ。"""
from importlib.metadata import version

__version__ = version("rjc-greet")


def greet(name: str) -> str:
    return f"こんにちは、{name}さん"
# src/rjc_greet/cli.py
import sys

from rjc_greet import __version__, greet


def main() -> None:
    name = sys.argv[1] if len(sys.argv) > 1 else "world"
    print(greet(name), f"(rjc-greet {__version__})")

各項目の意味

項目 意味 書き方の注意
[build-system] ビルドに使う道具(ビルドバックエンド) hatchling・setuptools・flit-core・pdm-backend・uv_build など
name PyPI での名前 英数字と . _ - だけ。先頭と末尾は英数字
version 版番号 PEP 440 の形。同じ版は二度と上げられない
description 1行の説明 PyPI の検索結果に出る
readme 説明ページの本文 拡張子で Markdown か reST かが決まる
requires-python 対応する Python pip が合わない版を飛ばすのに使う
license SPDX のライセンス式 "MIT"・"Apache-2.0" など
license-files 同梱するライセンスファイル glob で指定
dependencies 実行に要るパッケージ "requests>=2.32" の形
optional-dependencies 追加の依存(extras) pip install rjc-greet[dev] で入る
[project.scripts] コマンド名と呼ぶ関数 入れると rjc-greet コマンドができる
[project.urls] PyPI の左側に出るリンク ソース・課題・ドキュメント

ビルドバックエンドは、チュートリアルでは Hatchling が例になっていて、setuptools・Flit・PDM・uv-build でも同じように動くと書かれています。setuptools にする場合は [build-system] だけを次のように変えます。

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

書き間違えたときのエラー(実物)

pyproject.toml の誤りは、ビルドの時点で止まります。hatchling 1.32.4 で、わざと間違えて python -m build した結果です。

name = "rjc greet!"
  ValueError: Required field `project.name` must only contain ASCII letters/digits, underscores, hyphens, and periods, and must begin and end with ASCII letters/digits.
version = "1.0 beta"
  ValueError: Invalid version `1.0 beta` from field `project.version`, see https://peps.python.org/pep-0440/
license = "MIT License"
  ValueError: Error parsing field `project.license` - Invalid license expression: 'MIT License'
readme = "README"(拡張子なし)
  TypeError: Unable to determine the content-type based on the extension of readme file: README

どれも build の出力の最後は ERROR Backend subprocess exited when trying to invoke build_sdist になるので、その上の Error: の行を探します。ライセンスは「MIT License」ではなく SPDX の識別子 MIT で書きます。

ビルド(python -m buildでsdistとwheelを作る)

この節では、python -m build が何を作るのか、sdist と wheel の違いが分かります。

python3 -m build
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling >= 1.26
* Getting build dependencies for sdist...
* Installed build dependency versions:
  - hatchling==1.32.4
* Building sdist...
* Building wheel from sdist
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
  - hatchling >= 1.26
* Getting build dependencies for wheel...
* Installed build dependency versions:
  - hatchling==1.32.4
* Building wheel...
Successfully built rjc_greet-0.1.0.tar.gz and rjc_greet-0.1.0-py3-none-any.whl

build は一時的な仮想環境を作り、[build-system] の requires に書いた道具をそこに入れてからビルドします。 先に sdist を作り、その sdist から wheel を作るので、sdist に入れ忘れたファイルがあると wheel にも入りません。

-rw-r--r-- 1 root root 2386 Oct 22 22:28 rjc_greet-0.1.0-py3-none-any.whl
-rw-r--r-- 1 root root 1337 Oct 22 22:28 rjc_greet-0.1.0.tar.gz

sdist と wheel

sdist(.tar.gz)ソースコード一式と pyproject.toml。入れる側でビルドが走る。C 拡張ならコンパイラが要る
→
wheel(.whl)ビルド済みの zip。入れる側は展開するだけで速い。pip は使える wheel があれば優先する

両方を上げるのが標準。純粋な Python なら wheel は py3-none-any の1つで済む

wheel のファイル名の読み方

rjc_greet-0.1.0-py3-none-any.whl
名前     -版    -Python-ABI -プラットフォーム
例 意味
py3-none-any Python 3 ならどの OS・CPU でも入る(純粋な Python)
cp311-cp311-manylinux_2_28_x86_64 CPython 3.11 専用・Linux(glibc 2.28 以上)・x86_64
cp312-cp312-win_amd64 CPython 3.12 専用・64ビット Windows
cp312-cp312-macosx_11_0_arm64 CPython 3.12 専用・Apple シリコンの macOS 11 以上

pip は、自分の環境で使えるタグの一覧と照らし合わせて wheel を選びます。この環境(CPython 3.11・Linux x86_64)で packaging の sys_tags() を数えると 989 個あり、先頭は cp311-cp311-linux_x86_64・cp311-cp311-manylinux_2_39_x86_64 で、最後が py30-none-any でした。合う wheel が1つも無ければ、pip は sdist を落として手元でビルドします。

wheelの中身(zipを開いて確かめる)

この節では、wheel が実はただの zip で、中に何が入っているのかを実物で確かめられます。

python3 -m zipfile -l dist/rjc_greet-0.1.0-py3-none-any.whl
File Name                                             Modified             Size
rjc_greet/__init__.py                          2020-02-02 00:00:00          209
rjc_greet/cli.py                               2020-02-02 00:00:00          185
rjc_greet-0.1.0.dist-info/METADATA             2020-02-02 00:00:00          731
rjc_greet-0.1.0.dist-info/WHEEL                2020-02-02 00:00:00           87
rjc_greet-0.1.0.dist-info/entry_points.txt     2020-02-02 00:00:00           49
rjc_greet-0.1.0.dist-info/licenses/LICENSE     2020-02-02 00:00:00          127
rjc_greet-0.1.0.dist-info/RECORD               2020-02-02 00:00:00          555

file コマンドの判定も Zip archive data, at least v2.0 to extract, compression method=deflate でした。日付が 2020-02-02 にそろっているのは、hatchling が同じソースから同じ wheel ができるように日付を固定しているためです。

1METADATA

名前・版・説明・依存・README の本文。PyPI のページと pip show はここを読む。

2WHEEL

wheel の形式の版と、作った道具、互換タグ(py3-none-any)。

3entry_points.txt

[project.scripts] の中身。pip はこれを見て rjc-greet コマンドを作る。

4RECORD

全ファイルの sha256 とサイズ。pip uninstall は、ここに載ったファイルを消す。

METADATA の中身です。pyproject.toml の各項目が、どの行になったかが分かります。

Metadata-Version: 2.5
Name: rjc-greet
Version: 0.1.0
Summary: 名前を受け取ってあいさつ文を返す小さなサンプル
Project-URL: Homepage, https://example.com/rjc-greet
Project-URL: Issues, https://example.com/rjc-greet/issues
Author-email: RJC Tech Blog <dev@example.com>
License-Expression: MIT
License-File: LICENSE
Keywords: greeting,sample
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# rjc-greet

名前を受け取ってあいさつ文を返すサンプルです。

```python
from rjc_greet import greet
print(greet("田中"))
```

readme = "README.md" から Description-Content-Type: text/markdown が作られ、README の本文がそのまま後ろに入ります。 PyPI のプロジェクトページに出る説明は、この部分です。

WHEEL・entry_points.txt・RECORD は次のとおりです。

--- WHEEL
Wheel-Version: 1.0
Generator: hatchling 1.32.4
Root-Is-Purelib: true
Tag: py3-none-any
--- entry_points.txt
[console_scripts]
rjc-greet = rjc_greet.cli:main
--- RECORD
rjc_greet/__init__.py,sha256=c_79B-pTwJfbAfKjKYI-1d1QWvwJ7PQP83sp6mUkkjk,209
rjc_greet/cli.py,sha256=D_k7QbYftXaEEtPOvsjIbsUJ44CD4A65AoTT24s9M90,185
rjc_greet-0.1.0.dist-info/METADATA,sha256=Hw2yQdWa2IzK7SZBES5QQXxEWv3ObHfnV7HWruXixEg,731
rjc_greet-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
rjc_greet-0.1.0.dist-info/entry_points.txt,sha256=aEhgE8PEAKijbAgFLwXhda_ExK2_kZYQVafRatGeEQ4,49
rjc_greet-0.1.0.dist-info/licenses/LICENSE,sha256=xeFSaco18b15hr923ExMW_u21AkkTiWCtErAE3w7bug,127
rjc_greet-0.1.0.dist-info/RECORD,,

sdist の中身も見ておきます。こちらにはテストや pyproject.toml も入り、メタデータは PKG-INFO という名前です。

$ tar tzvf dist/rjc_greet-0.1.0.tar.gz
-rw-r--r-- 0/0             209 2020-02-02 09:00 rjc_greet-0.1.0/src/rjc_greet/__init__.py
-rw-r--r-- 0/0             185 2020-02-02 09:00 rjc_greet-0.1.0/src/rjc_greet/cli.py
-rw-r--r-- 0/0             110 2020-02-02 09:00 rjc_greet-0.1.0/tests/test_greet.py
-rw-r--r-- 0/0             127 2020-02-02 09:00 rjc_greet-0.1.0/LICENSE
-rw-r--r-- 0/0             149 2020-02-02 09:00 rjc_greet-0.1.0/README.md
-rw-r--r-- 0/0             745 2020-02-02 09:00 rjc_greet-0.1.0/pyproject.toml
-rw-r--r-- 0/0             731 2020-02-02 09:00 rjc_greet-0.1.0/PKG-INFO

公開する前に、wheel と sdist に秘密の情報(.env・鍵・社内の設定)が入っていないかを、この一覧で確かめます。 PyPI に上げたファイルは誰でもダウンロードできます。

twineでアップロードする(ローカルのインデックスで実演)

この節では、twine のアップロードの実際の出力と、送られる中身、失敗したときの表示が分かります。

本物の PyPI に練習用のパッケージを増やさないよう、手元に pypiserver(パスワード認証つき)を 127.0.0.1:34400 で立てて試しました。pypiserver の起動は次の形です。

python3 -m pip install pypiserver passlib
pypi-server run -p 34400 -i 127.0.0.1 -P htpasswd -a update --disable-fallback packages

-a update は「アップロードのときだけ認証する」、--disable-fallback は「無いパッケージを PyPI に転送しない」という指定です。

まず twine check で README を確かめる

$ twine check dist/*
Checking dist/rjc_greet-0.1.0-py3-none-any.whl: PASSED
Checking dist/rjc_greet-0.1.0.tar.gz: PASSED

--strict を付けると、警告でも失敗(終了コード 1)にできます。CI に入れるなら twine check --strict dist/* が使えます。

アップロードの成功・失敗の出力

1

パスワードを間違える → 403

ERROR HTTPError: 403 Forbidden。ファイルは1つも置かれない。

2

正しい認証 → 2ファイルとも成功

wheel(5.4 kB)と sdist(4.3 kB)を順に送る。

3

同じ版をもう一度 → 400

同じファイル名はすでにある、という理由で拒否される。

$ twine upload --repository-url http://127.0.0.1:34400/ -u uploader -p wrong dist/*
Uploading distributions to http://127.0.0.1:34400/
Uploading rjc_greet-0.1.0-py3-none-any.whl
WARNING  Error during upload. Retry with the --verbose option for more details.
ERROR    HTTPError: 403 Forbidden from http://127.0.0.1:34400/
         Forbidden

$ twine upload --repository-url http://127.0.0.1:34400/ -u uploader -p ******** dist/*
Uploading distributions to http://127.0.0.1:34400/
Uploading rjc_greet-0.1.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5.4/5.4 kB • 00:00 • ?
Uploading rjc_greet-0.1.0.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.3/4.3 kB • 00:00 • ?

同じ版をもう一度上げ、--verbose で理由を見た結果です。

                     <h1>Error: 400 Bad Request</h1>
                     <pre>Package &#039;rjc_greet-0.1.0-py3-none-any.whl&#039;
         already exists!
           You may start server with `--overwrite` option.</pre>
ERROR    HTTPError: 400 Bad Request from http://127.0.0.1:34400/
         Bad Request

本物の PyPI でも同じ版を上げ直すと 400 になり、PyPI のソースでは File already exists ('ファイル名', with blake2_256 hash '…'). という文言です。PyPI には上書きの方法がありません。 直すときは版番号を上げて、もう一度ビルドします。

--skip-existing(すでにあるファイルは飛ばす)は、twine のドキュメントで PyPI 向けの機能とされています。pypiserver に付けると、twine は送る前に止まりました。

ERROR    UnsupportedConfiguration: The configured repository
         'http://127.0.0.1:34400/' does not have support for the following
         features: --skip-existing and is an unsupported configuration

twine が送っている中身

自作の受け口(127.0.0.1:34401)に twine を向けて、アップロード1回で送られる項目を見ました。ユーザー名は __token__、パスワードはダミーのトークンです。

POST /legacy/
Authorization: Basic (user = __token__)
Content-Type: multipart/form-data
  provides_extra         dev
  summary                名前を受け取ってあいさつ文を返す小さなサンプル
  requires_python        >=3.10
  name                   rjc-greet
  metadata_version       2.5
  keywords               greeting, sample
  license_expression     MIT
  version                0.1.1
  license_file           LICENSE
  classifiers            Operating System :: OS Independent
  classifiers            Programming Language :: Python :: 3
  project_urls           Homepage, https://example.com/rjc-greet
  project_urls           Issues, https://example.com/rjc-greet/issues
  requires_dist          pytest>=8; extra == 'dev'
  author_email           RJC Tech Blog <dev@example.com>
  description_content_type text/markdown
  description            (149 バイト)
  pyversion              py3
  filetype               bdist_wheel
  sha256_digest          658b3787d301d8022e5a660f61e5478596fd7659565667df38a58d7e1476001b
  blake2_256_digest      25374588c0d4a21cf1db1719b63c4f1752a9738c9444c2a3230ec0c7f8cc50f5
  :action                file_upload
  protocol_version       1
  content                <ファイル rjc_greet-0.1.1-py3-none-any.whl 2385 バイト>

twine は METADATA の中身をフォームの項目に分け、ファイル本体とハッシュを付けて、ベーシック認証で1ファイルずつ POST します。 PyPI の受け口は https://upload.pypi.org/legacy/、TestPyPI は https://test.pypi.org/legacy/ です。

入れて動くかを確かめる

別の仮想環境から、手元のインデックスを指定して入れました。

$ pip install --index-url http://127.0.0.1:34400/simple/ rjc-greet
Looking in indexes: http://127.0.0.1:34400/simple/
Collecting rjc-greet
  Downloading rjc_greet-0.1.1-py3-none-any.whl (2.4 kB)
Installing collected packages: rjc-greet
Successfully installed rjc-greet-0.1.1

$ rjc-greet 田中
こんにちは、田中さん (rjc-greet 0.1.1)

$ pip index versions --index-url http://127.0.0.1:34400/simple/ rjc-greet
rjc-greet (0.1.1)
Available versions: 0.1.1, 0.1.0
  INSTALLED: 0.1.1
  LATEST:    0.1.1

[project.scripts] に書いた rjc-greet コマンドができ、__version__ は importlib.metadata から 0.1.1 が読めています。無い版を指定したときは、次のエラーでした。

ERROR: Could not find a version that satisfies the requirement rjc-greet==9.9.9 (from versions: 0.1.0, 0.1.1)
ERROR: No matching distribution found for rjc-greet==9.9.9

公開した直後に入れる側でこのエラーが出るときの原因と直し方は、関連記事「pip No matching distribution foundの直し方」で扱っています。

TestPyPIで練習する

この節では、本番の前に TestPyPI で試す手順と、本番との違いが分かります。

TestPyPI(test.pypi.org)

練習用。アカウントとトークンは PyPI と別

ときどきパッケージやアカウントが消される

twine は -r testpypi で上げる

ほかのパッケージがそろっていないので、入れるときは --no-deps

PyPI(pypi.org)

本番。pip install 名前 で誰でも入れられる

上げたファイル名は消しても再利用できない

twine は -r 指定なしで上げる

2要素認証が必須

# 上げる(トークンを聞かれたら pypi- から始まる TestPyPI のトークンを貼る)
python3 -m twine upload --repository testpypi dist/*

# 別の仮想環境で入れて確かめる
python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps rjc-greet

Python Packaging User Guide のチュートリアルでは、TestPyPI は TestPyPI 以外のパッケージを持っていないので --no-deps を付けています。依存も PyPI から入れて試したいときに --extra-index-url https://pypi.org/simple/ を足す方法もありますが、2つのインデックスに同じ名前があると、pip はどちらからでも版の大きいほうを選びます。社内用の名前を公開の PyPI でも取られると、そちらが入ってしまう(依存関係の取り違え)点に注意が要ります。

APIトークンと.pypircの設定

この節では、手元からアップロードするときの認証の設定と、トークンを安全に置く方法が分かります。

PyPI へのアップロードは、ユーザー名 __token__、パスワードに API トークン(pypi- で始まる文字列の全体)を使います。 PyPI のヘルプの手順は、メールアドレスを確認したうえで、アカウント設定の「API tokens」の欄で「Add API token」を選ぶ形です。

スコープ 使える範囲 使いどころ
Entire account アカウントの全プロジェクト 初めての公開(プロジェクトがまだ無い)
1つのプロジェクト そのプロジェクトだけ 2回目以降・CI。ヘルプも最小の範囲を勧めている

初めての公開はプロジェクトがまだ無いので、アカウント全体のトークンで上げます。公開できたら、そのプロジェクトだけのトークンを作り直し、全体のトークンは削除すると、漏れたときの被害を小さくできます。

認証の渡し方(3通り)

# ~/.pypirc(chmod 600 で自分だけが読めるようにする)
[distutils]
index-servers =
    pypi
    testpypi

[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmc...(PyPI のトークン)

[testpypi]
username = __token__
password = pypi-AgENdGVzdC5weXBp...(TestPyPI のトークン)
# 環境変数で渡す(CI 向け。履歴に残らないようにシークレットから入れる)
export TWINE_USERNAME=__token__
export TWINE_PASSWORD="$PYPI_TOKEN"
python3 -m twine upload dist/*

# keyring に入れる(ファイルに平文で書かない)
keyring set https://upload.pypi.org/legacy/ __token__

.pypirc の仕様のページでも、このファイルはパスワードを平文で持つので、keyring・環境変数を使うか、chmod 600 ~/.pypirc で自分だけが読めるようにする、と書かれています。

.pypirc の書式は、手元のインデックスに名前を付けて -r で選ぶ形でも確かめました(別の場所のファイルを --config-file で指定)。

[distutils]
index-servers =
    local

[local]
repository = http://127.0.0.1:34400/
username = uploader
password = ********
$ twine upload --verbose --config-file ./pypirc-local -r local dist/*
INFO     Using configuration from ./pypirc-local
Uploading distributions to http://127.0.0.1:34400/
INFO     dist/rjc_greet-0.1.1-py3-none-any.whl (2.3 KB)
INFO     dist/rjc_greet-0.1.1.tar.gz (1.3 KB)
INFO     username set from config file
INFO     password set from config file
INFO     username: uploader
INFO     password: <hidden>
Uploading rjc_greet-0.1.1-py3-none-any.whl
INFO     Response from http://127.0.0.1:34400/:
         200 OK
Uploading rjc_greet-0.1.1.tar.gz
INFO     Response from http://127.0.0.1:34400/:
         200 OK

--verbose を付けると、どの設定ファイルから、どのユーザー名を使ったかが出ます。パスワードは <hidden> と表示されます。認証で失敗したときは、まずこの行を見ると原因を絞れます。

twine 7.0.0 の主なオプションです。

オプション・環境変数 意味
-r・--repository .pypirc の名前(pypi・testpypi など)
--repository-url アップロード先の URL(-r より優先)
-u・-p ユーザー名・パスワード
--skip-existing すでにあるファイルを飛ばす(PyPI 向け)
--verbose 応答の中身まで表示
--non-interactive 認証情報が無くても入力を求めない
--attestations 各ファイルの証明(attestation)も上げる
TWINE_USERNAME・TWINE_PASSWORD 認証情報。コマンドの引数のほうが優先

Trusted Publishing(GitHub Actionsから公開する)

この節では、トークンを GitHub に保存せずに、タグを打つだけで PyPI に公開する設定が分かります。

Trusted Publishing は、GitHub Actions などの CI が発行する OIDC の短い期限の身分証を、PyPI が「このリポジトリのこのワークフローなら公開してよい」と照合する仕組みです。 長く使える API トークンをシークレットに保存しないので、漏れる心配が減ります。

flowchart TD
    T[タグを push] --> W[GitHub Actions<br>publish.yml が動く]
    W --> O[GitHub が OIDC の<br>身分証を発行]
    O --> P[PyPI が設定と照合<br>リポジトリ・ワークフロー・環境]
    P --> K[短い期限の<br>トークンを発行]
    K --> U[アップロード]

PyPI 側の設定

docs.pypi.org の手順です。

  • すでにあるプロジェクト:「Your projects」でプロジェクトの「Manage」→ 左の「Publishing」→ GitHub Actions を選び、入力して「Add」。
  • まだ無いプロジェクト:アカウントの左の「Publishing」で「pending publisher」(保留中の公開元)として、作る PyPI のプロジェクト名と一緒に登録します。最初に公開したときにプロジェクトができ、普通の公開元に変わります。
入力する項目 例 必須か
PyPI のプロジェクト名(新規のみ) rjc-greet 新規は必須
リポジトリの所有者 octo-org 必須
リポジトリ名 rjc-greet 必須
ワークフローのファイル名 publish.yml 必須
環境名(Environment) pypi 任意(強く推奨)

docs.pypi.org によると、pending publisher は実際に公開するまで名前を確保しません。その前に別の人がその名前を登録すると、登録した pending publisher は無効になります。TestPyPI でも使うときは、TestPyPI 側に別に登録します(環境名は testpypi など)。

ワークフロー(.github/workflows/publish.yml)

Python Packaging User Guide の例をもとに、v で始まるタグを push したときだけ PyPI に公開する形にしたものです。YAML として読み込め、ジョブの依存と権限が意図どおりであることを PyYAML で確かめました。

name: Publish to PyPI

on:
  push:
    tags:
      - "v*"

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false
      - uses: actions/setup-python@v6
        with:
          python-version: "3.x"
      - run: python3 -m pip install build
      - run: python3 -m build
      - uses: actions/upload-artifact@v5
        with:
          name: python-package-distributions
          path: dist/

  publish-to-pypi:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/rjc-greet
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v6
        with:
          name: python-package-distributions
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1
  • 1permissions に id-token: write があるdocs.pypi.org で Trusted Publishing に必須とされている。ジョブ単位で付けるのが推奨
  • 2ビルドと公開を別のジョブに分けているpypa/gh-action-pypi-publish の README は、ビルドを権限の小さい別ジョブで行うよう勧めている
  • 3公開のジョブは Linux のランナーこのアクションは Docker を使うので GNU/Linux のジョブでしか動かない
  • 4環境名とワークフロー名が PyPI の登録と一致publish.yml・pypi の綴りが違うと照合に失敗する
  • 5GitHub の pypi 環境に承認者を付ける公式の手引きは、手動の承認を必須にすることを勧めている

TestPyPI に上げるジョブを足すときは、公開のステップに with: で repository-url: https://test.pypi.org/legacy/ を指定し、環境名を testpypi にします。

アクションの README によると、Trusted Publishing で公開すると、ファイルごとの証明(attestation)が既定で作られて一緒に上がります(attestations: false で止められます)。前の節で見た twine の JSON の provenance が、その証明の置き場所です。トークンで公開していたワークフローから移すときは、username: __token__ と password: ${{ secrets.PYPI_TOKEN }} を消し、environment と permissions を足します。

GitHub Actions そのものの書き方は、関連記事「CI/CDとは|GitHub Actionsで最初のパイプラインを作るまで」で扱っています。

版番号の付け方(PEP 440とセマンティックバージョニング)

この節では、PyPI で使える版番号の形と、上げ方の目安が分かります。

PyPI の版番号は PEP 440 の形に従います。packaging 26.3 の Version で、いろいろな書き方を読ませた結果です。

from packaging.version import Version, InvalidVersion
for v in ["1.0.0", "1.0.0a1", "1.0.0b2", "1.0.0rc1", "1.0.0.post1", "1.0.0.dev3",
          "v1.0", "1.0-beta", "1.0.0+local.1", "2026.10.22", "1.0 beta"]:
    try:
        x = Version(v); print(f"{v:15} -> {str(x):14} pre={x.is_prerelease}")
    except InvalidVersion as e:
        print(f"{v:15} -> InvalidVersion: {e}")
1.0.0           -> 1.0.0          pre=False
1.0.0a1         -> 1.0.0a1        pre=True
1.0.0b2         -> 1.0.0b2        pre=True
1.0.0rc1        -> 1.0.0rc1       pre=True
1.0.0.post1     -> 1.0.0.post1    pre=False
1.0.0.dev3      -> 1.0.0.dev3     pre=True
v1.0            -> 1.0            pre=False
1.0-beta        -> 1.0b0          pre=True
1.0.0+local.1   -> 1.0.0+local.1  pre=False
2026.10.22      -> 2026.10.22     pre=False
1.0 beta        -> InvalidVersion: Invalid version: '1.0 beta'

並べたときの順番は次のとおりでした。

0.9.9 < 1.0.0.dev3 < 1.0.0a1 < 1.0.0b2 < 1.0.0rc1 < 1.0.0 < 1.0.0.post1 < 1.0.1
形 意味 pip install 名前 で入るか
1.2.3 正式版(メジャー.マイナー.パッチ) 入る
1.2.3a1・b1・rc1 アルファ・ベータ・リリース候補 入らない(--pre か版の指定が要る)
1.2.3.dev1 開発版 入らない(同上)
1.2.3.post1 中身を変えない後からの修正(説明の直しなど) 入る
1.2.3+local ローカル版 PyPI には上げられない

よく使われるのは、セマンティックバージョニングに沿った「互換性を壊したらメジャー、機能を足したらマイナー、不具合の修正はパッチを上げる」付け方です。 1.0.0 より前(0.x)は「まだ仕様が固まっていない」という意味で使われることが多くあります。日付の形(2026.10.22)も PEP 440 では有効です。

版番号を1か所で管理したいときは、hatchling では dynamic と [tool.hatch.version] を使います。次の設定で __about__.py の __version__ = "0.2.0" が読まれ、rjc_greet-0.2.0.tar.gz and rjc_greet-0.2.0-py3-none-any.whl ができました。

[project]
name = "rjc-greet"
dynamic = ["version"]

[tool.hatch.version]
path = "src/rjc_greet/__about__.py"

uv・Poetryで公開する場合

この節では、twine 以外の道具で公開するときのコマンドの対応が分かります。

やること build+twine uv Poetry
ビルド python -m build uv build poetry build
公開 twine upload dist/* uv publish poetry publish
トークンの渡し方 TWINE_PASSWORD --token・UV_PUBLISH_TOKEN poetry config pypi-token.pypi
Trusted Publishing gh-action-pypi-publish GitHub Actions なら認証の設定なし gh-action-pypi-publish

uv のドキュメントでは、uv build は dist/ に成果物を置き、GitHub Actions などの Trusted Publisher から公開するときは認証情報の設定が要らないとされています。TestPyPI に上げるときは pyproject.toml に次を書き、uv publish --index testpypi で上げます。

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

uv の使い方全体は関連記事「uvの使い方|pip・venvからの移行とチーム開発での使い方」、Poetry は関連記事「Poetry・asdfの使い方|違い・組み合わせ方とエラーの直し方」で扱っています。

PyPI公開でよくあるエラーと直し方

この節では、ビルド・アップロードで出るエラーの原文から、原因と直し方を引けます。PyPI 側の文言は PyPI のソース(pypi/warehouse)とヘルプで確認したものです。

エラー(原文) 原因 直し方
400 File already exists ('…', with blake2_256 hash '…'). 同じ版のファイルがすでにある 版番号を上げてビルドし直す
400 This filename was previously used by a file that has since been deleted. Use a different version. 消したファイルと同じ名前 消しても名前は再利用できない。版を上げる
400 The name '…' is too similar to an existing project. 既存の名前と紛らわしい 別の名前にする
400 The name '…' isn't allowed (conflict with Python Standard Library module name). 標準ライブラリと同じ名前 別の名前にする
403 Invalid or non-existent authentication information. トークンの誤り・失効・改行の混入、ユーザー名が __token__ でない トークンを貼り直し、ユーザー名を __token__ にする
403 The user '…' isn't allowed to upload to project '…'. 名前が他人のプロジェクト 別の名前にする。自分のなら所有者に招待してもらう
403 The given token isn't allowed to upload to project '…'. トークンのスコープが別のプロジェクト そのプロジェクト用のトークンを使う
403 … does not have two-factor authentication enabled. 2要素認証が無効 アカウントで2要素認証を有効にする
400 The description failed to render … README の書式の誤り・種類の指定違い twine check で直してから上げる
400 File too large. Limit for project '…' is 100 MB. 1ファイルの上限(既定 100MiB)を超えた 不要なファイルを外す。必要なら上限の引き上げを申請

README が PyPI で表示されない・崩れる

原因は、ほぼ次の3つです。

  • readme に README.md を指定していない:description の1行だけがページに出ます。readme = "README.md" を書きます。
  • Markdown なのに reStructuredText として扱われている:pyproject.toml では拡張子で決まり、.md は text/markdown、.rst は text/x-rst になります。拡張子が無いファイルは、上のとおりビルドで止まります。
  • reStructuredText の書式の誤り:見出しの下線の長さが足りないだけでも失敗します。

わざと見出しの下線を短くした README.rst で twine check した結果です。

Checking dist/rjc_greet-0.1.1-py3-none-any.whl: FAILED
ERROR    `long_description` has syntax errors in markup and would not be
         rendered on PyPI.
         line 2: Warning: Title underline too short.

         rjc-greet
         =====

PyPI は、上げたあとで README を直せません。 説明を直すには版を上げるか、1.0.0.post1 のような後からの修正版を出します。上げる前に twine check --strict で止めるのが確実です。README の中の画像やリンクは、PyPI のページからも見えるように https:// で始まる絶対 URL で書きます。

入れる側で起きること

症状 原因 直し方
公開直後に No matching distribution found 版の指定違い・Python の版が requires-python に合わない pip index versions 名前 で版を確かめる
pip install で新しい版が入らない キャッシュ・別のインデックスを見ている pip install --no-cache-dir、Looking in indexes の行を見る
import できない 配布名と import 名の取り違え、src/ のパッケージが wheel に入っていない python -m zipfile -l で wheel の中身を見る
コマンドが無い [project.scripts] が無い・関数名の誤り entry_points.txt を見る

公開前のチェックリスト

この節では、本番の PyPI に上げる直前に確かめることを一覧で確認できます。

  • 1名前が空いていて、紛らわしくないpypi.org/project/名前/ を開き、正規化した形でも確かめる
  • 2版番号が今までに上げたことのない値消したファイルの版も使えない
  • 3dist/ に今回の版のファイルだけがあるrm -rf dist してからビルドすると、前の版を一緒に上げずに済む
  • 4twine check --strict が PASSEDREADME が PyPI で表示できる
  • 5wheel と sdist に秘密の情報が入っていないpython -m zipfile -l と tar tzvf で一覧を見る
  • 6新しい仮想環境で wheel から入れて動くpip install dist/名前-版-py3-none-any.whl のあと import とコマンドを試す
  • 7ライセンスが SPDX の形で書かれ、LICENSE が入っているwheel の dist-info/licenses/ にある
  • 8TestPyPI で一度通したページの表示と pip install を確かめる

公開したあと(更新・yank・削除)

この節では、新しい版の出し方と、問題のある版を引っ込める方法が分かります。

  • 更新:pyproject.toml の版番号を上げ、rm -rf dist → python -m build → twine upload dist/*。PyPI のページの「Release history」に版が積み上がります。
  • yank(ヤンク):壊れた版・脆弱性のある版を「通常は入らない」状態にします。docs.pypi.org によると、プロジェクトの releases の管理ページで版の「Options」→「Yank」で行い、理由も書けます。yank した版は、==1.0.0 のように完全に指定されたときだけ入ります。ファイルは消えないので、その版に固定している利用者は壊れません。
  • 削除:版やプロジェクトを消すこともできますが、PyPI のヘルプにあるとおり、同じファイル名は二度と使えません。利用者への影響が大きいので、docs.pypi.org は yank を削除の代わりになる壊さない方法として案内しています。

ファイルの大きさには上限があり、PyPI のヘルプでは1ファイル 100.0 MiB、プロジェクト全体で 10.0 GiB が既定です(2026年10月22日時点)。

よくある質問

Q. PyPIの読み方は? A. 「パイピーアイ」です。PyPI のヘルプでも「pie pea eye」と1文字ずつ読むとされています。

Q. PyPIとpipの違いは? A. PyPI はパッケージの置き場(サーバー)で、pip はそこからパッケージを入れるコマンドです。pip は既定で https://pypi.org/simple を見に行きます。

Q. PyPIへの登録(公開)は無料ですか? A. 無料です。アカウントを作り、メールアドレスを確認して2要素認証を有効にすれば公開できます。

Q. PyPIに公開する最低限のファイルは? A. pyproject.toml と、パッケージのコード(src/パッケージ名/init.py)です。README.md と LICENSE も入れるのが一般的です。

Q. python -m build と python setup.py sdist の違いは? A. build は pyproject.toml の [build-system] に従って、隔離した環境でどのビルドバックエンドでもビルドします。setup.py を直接実行する方法は setuptools 専用で、今の公式の手引きは build を使っています。

Q. TestPyPIとPyPIは同じアカウントで使えますか? A. 使えません。TestPyPI は別のサイトで、アカウントと API トークンも別に作ります。Trusted Publishing の設定も、それぞれに登録します。

Q. APIトークンのユーザー名は何を入れますか? A. __token__ です。パスワードには pypi- から始まるトークンの全体を入れます。

Q. 同じバージョンを上書きアップロードできますか? A. できません。400 の File already exists になります。消したあとでも同じファイル名は使えないので、版番号を上げます。

Q. PyPIにアップロードしたREADMEが表示されないのはなぜ? A. pyproject.toml に readme が無いか、拡張子と中身の書式が合っていないか、reStructuredText の書式の誤りです。上げる前に twine check --strict dist/* で確かめます。

Q. Trusted Publishingを使うとAPIトークンは要りませんか? A. 要りません。GitHub Actions のワークフローに id-token: write の権限を付け、PyPI にリポジトリ・ワークフロー名・環境名を登録すると、公開のたびに短い期限のトークンが自動で発行されます。

Q. 社内だけで使うパッケージを置く場所は? A. 公開の PyPI ではなく、pypiserver のような自前のインデックスや、各クラウドのパッケージリポジトリを使います。pip は --index-url でそこを指定します。社内の名前と同じ名前が公開の PyPI にあると取り違えが起きるので、--extra-index-url で混ぜるときは注意が要ります。

Q. 公開したパッケージを消すには? A. 管理画面から版やプロジェクトを削除できますが、同じファイル名は二度と使えません。壊れた版を入らないようにしたいだけなら、yank を使います。

まとめ

  • PyPI は pip が既定で取りに行く Python の公式のパッケージ置き場。pip は /simple/名前/ のページからファイルを選び、ハッシュを確かめて入れる。
  • 公開は「pyproject.toml → python -m build(sdist と wheel)→ twine check → twine upload」。練習は TestPyPI で。
  • wheel はただの zip で、METADATA・WHEEL・entry_points.txt・RECORD が入っている。上げる前に中身を一覧で確かめる。
  • 認証はユーザー名 __token__ とトークン。CI からは Trusted Publishing(id-token: write)でトークンを保存せずに公開できる。
  • 版番号は PEP 440。同じ版は上書きできず、消したファイル名も再利用できない。
  • README の崩れは上げてからでは直せないので、twine check --strict で止める。

参考・出典

確認日はいずれも 2026年10月22日です。

  • PyPI:https://pypi.org/
  • PyPI Help:https://pypi.org/help/
  • PyPI Docs「Adding a Trusted Publisher to an Existing PyPI Project」:https://docs.pypi.org/trusted-publishers/adding-a-publisher/
  • PyPI Docs「Creating a PyPI Project with a Trusted Publisher」:https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/
  • PyPI Docs「Publishing with a Trusted Publisher」:https://docs.pypi.org/trusted-publishers/using-a-publisher/
  • PyPI Docs「Yanking」:https://docs.pypi.org/project-management/yanking/
  • Python Packaging User Guide「Packaging Python Projects」:https://packaging.python.org/en/latest/tutorials/packaging-projects/
  • Python Packaging User Guide「Publishing package distribution releases using GitHub Actions CI/CD workflows」:https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
  • Python Packaging User Guide「The .pypirc file」:https://packaging.python.org/en/latest/specifications/pypirc/
  • Python Packaging User Guide「Simple repository API」:https://packaging.python.org/en/latest/specifications/simple-repository-api/
  • PEP 440「Version Identification and Dependency Specification」:https://peps.python.org/pep-0440/
  • twine documentation:https://twine.readthedocs.io/en/stable/
  • pypa/gh-action-pypi-publish:https://github.com/pypa/gh-action-pypi-publish
  • uv「Building and publishing a package」:https://docs.astral.sh/uv/guides/package/
  • pypi/warehouse ソース「warehouse/forklift/legacy.py」:https://github.com/pypi/warehouse/blob/main/warehouse/forklift/legacy.py
  • pypi/warehouse ソース「warehouse/packaging/services.py」:https://github.com/pypi/warehouse/blob/main/warehouse/packaging/services.py
  • pypiserver:https://github.com/pypiserver/pypiserver

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PyPIとは|パッケージ公開の手順・pyproject.toml・エラーを実演」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?