diff --git a/README.md b/README.md
index 0005845..e4c2b9d 100644
--- a/README.md
+++ b/README.md
@@ -232,6 +232,8 @@ Squash-merging PRs with Conventional Commits titles? Each PR is already one comm
Want the release-please result (version bump, CHANGELOG.md, tag, GitHub Release) without a release PR waiting in your PR list? Run the same script from a one-button `workflow_dispatch` job: [release-please alternative without a release PR](https://loki-inu.github.io/relnote/release-please-alternative.html). From v0.3.6, a version `--changelog-title` such as `v1.4.0` on an untagged `HEAD` makes the CHANGELOG.md compare link end at that tag, so `release.sh` no longer writes `...HEAD` there.
+Releasing a Python package? [Release a Python package from conventional commits](https://loki-inu.github.io/relnote/python-package.html) bumps `version` in `pyproject.toml`, prepends CHANGELOG.md, and tags with `release.sh`, then a tag-push workflow checks the tag against `pyproject.toml`, builds, uploads to PyPI with trusted publishing, and creates the GitHub Release with relnote notes.
+
```sh
./release.sh --dry-run --bump-file package.json
./release.sh --bump-file package.json && git push --follow-tags
diff --git a/docs/index.html b/docs/index.html
index 084fc05..6443ebc 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -245,6 +245,7 @@
relnote
· standard-version alternative
· Squash-merged PR titles
· release-please alternative (no release PR)
+ · Python package releases (pyproject.toml, PyPI)
· Windows and PowerShell
diff --git a/docs/llms.txt b/docs/llms.txt
index c1f0c13..c21ee7a 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -24,6 +24,10 @@ standard-version is deprecated. examples/release.sh (bash, in the relnote repo)
No release PR bot: a workflow_dispatch job runs the relnote action (has-changes gate), then examples/release.sh --bump-file pyproject.toml (next version, version bump, CHANGELOG.md, chore(release) commit, annotated tag), pushes the commit and tag, and publishes relnote output with gh release create. From v0.3.6 the CHANGELOG.md compare link ends at the --changelog-title tag (e.g. v1.4.0) when HEAD is not tagged yet. Keep release-please for reviewed release PRs, per-language version updaters, or a protected main without a bypass token. Guide: https://loki-inu.github.io/relnote/release-please-alternative.html
+## Python packages (pyproject.toml, PyPI)
+
+Locally: examples/release.sh --bump-file pyproject.toml (first version = "..." line, so [project] or [tool.poetry]) → next version from commits, CHANGELOG.md section, chore(release) commit, annotated tag; git push --follow-tags. Tag-push workflow: check v$version == tag via tomllib, python -m build, pypa/gh-action-pypi-publish with trusted publishing (environment pypi, id-token: write only on that job), then gh release create TAG dist/* --notes-file with the loki-inu/relnote action's notes. Dynamic versions (setuptools-scm, hatch-vcs): skip --bump-file; the tag is the version. __version__: importlib.metadata.version("pkg"). Guide: https://loki-inu.github.io/relnote/python-package.html
+
## Compared to nearby tools
- git-cliff: keep it for templated CHANGELOG.md rebuilt from history and version bump. relnote prints a Release body, can prepend one release to CHANGELOG.md, and needs no config.
@@ -60,6 +64,7 @@ gh relnote prints the notes; gh relnote create TAG [gh release create flags] [--
- GitLab CI, Jenkins or any CI (single-file download): https://loki-inu.github.io/relnote/ci.html
- standard-version alternative (examples/release.sh): https://loki-inu.github.io/relnote/standard-version-alternative.html
- release-please alternative without a release PR (workflow_dispatch: bump version file, CHANGELOG.md, chore(release) commit, tag, gh release create; tested example): https://loki-inu.github.io/relnote/release-please-alternative.html
+- Release a Python package (pyproject.toml bump, CHANGELOG.md, tag, PyPI trusted publishing, GitHub Release; tested example): https://loki-inu.github.io/relnote/python-package.html
- Release notes from squash-merged PR titles (feat: x (#41); PR-title check; CHANGELOG.md PR links from v0.3.5): https://loki-inu.github.io/relnote/squash-merge.html
- Windows and PowerShell (Scoop, --output vs > redirection, UTF-8/BOM, line endings): https://loki-inu.github.io/relnote/windows.html
- Compare (git-cliff / gh --generate-notes / swapnanil/relnotes): https://loki-inu.github.io/relnote/compare.html
diff --git a/docs/python-package.html b/docs/python-package.html
new file mode 100644
index 0000000..469cc79
--- /dev/null
+++ b/docs/python-package.html
@@ -0,0 +1,402 @@
+
+
+
+
+
+
+ Release a Python package from conventional commits — relnote
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Release a Python package from conventional commits
+
Bump version in pyproject.toml, prepend a CHANGELOG.md section, and tag in one local command. Pushing the tag builds the sdist and wheel, uploads them to PyPI with trusted publishing, and creates a GitHub Release with grouped notes. No Node, no release bot, no config file; relnote itself is stdlib Python.
Any build backend works (Hatch, setuptools, PDM, flit, uv, Poetry). The version lives in one place; see the FAQ for __version__ and for dynamic versions.
+
+
2. Cut the release locally
+
Three commits since v1.2.0:
+
git log --oneline v1.2.0..HEAD
+docs: install with pipx
+fix: read config files as UTF-8 on Windows
+feat(cli): add --json output
+
Install relnote (pipx install git+https://github.com/loki-inu/relnote.git, brew install loki-inu/tap/relnote, or the single file), then run the release script pinned to a version:
A feat made it a minor bump. The release commit changes one line of pyproject.toml:
+
-version = "1.2.0"
++version = "1.3.0"
+
and adds this section to CHANGELOG.md:
+
## [v1.3.0] - 2026-10-29
+
+### Features
+
+- **cli:** add --json output (`4e39256`) — Ada Lovelace
+
+### Fixes
+
+- read config files as UTF-8 on Windows (`a8cc6d8`) — Ada Lovelace
+
+### Other
+
+- **docs:** install with pipx (`83256ad`) — Ada Lovelace
+
+**Full changelog:** https://github.com/you/demo/compare/v1.2.0...v1.3.0
+
Output from a test repository built in relnote's CI (tests/test_python_package_docs.py); your hashes and date differ. Use --dry-run first to see the version and notes without changing anything. Then:
+
git push --follow-tags
+
+
3. Build, publish, and release on the tag
+
Save as .github/workflows/release.yml. On PyPI, add a trusted publisher for the project with workflow release.yml and environment pypi, and create that environment in the repository settings.
The first step stops the run if the tag and pyproject.toml disagree. The upload job is the only one with id-token: write, and the GitHub Release is created only after PyPI accepted the files, with the wheel and sdist attached. The Release body is the action's notes output, passed through env: because commit subjects can contain backticks:
+
## What's changed
+
+### Features
+
+- **cli:** add --json output (`4e39256`) — Ada Lovelace
+
+### Fixes
+
+- read config files as UTF-8 on Windows (`a8cc6d8`) — Ada Lovelace
+
+### Other
+
+- **docs:** install with pipx (`83256ad`) — Ada Lovelace
+
+---
+
+Range: `v1.2.0` → `v1.3.0`.
+
+**Full changelog:** https://github.com/you/demo/compare/v1.2.0...v1.3.0
+
The chore(release): v1.3.0 commit is left out of the notes by default.
+
+
FAQ
+
Does this work with Poetry, Hatch, PDM, setuptools, or uv?
+
Yes, as long as pyproject.toml has a static version line. release.sh rewrites the first line that starts with version = "...", which is [project] version for Hatch, PDM, setuptools, flit, and uv, and [tool.poetry] version for Poetry 1.x. Keep that table above any other table with its own version key.
+
My version is dynamic (setuptools-scm or hatch-vcs). What changes?
+
Skip --bump-file and the tag check. The tag is the version, so run relnote --next-version, tag, and push; the build backend reads the version from the tag. The notes, CHANGELOG.md, and workflow steps stay the same.
+
How do I keep __version__ in sync?
+
Do not store it twice. In your package's __init__.py, read it from the installed metadata: from importlib.metadata import version; __version__ = version("demo").
+
from importlib.metadata import version
+
+__version__ = version("demo")
+
Why trusted publishing instead of a PyPI token?
+
PyPI trusted publishing lets the workflow get a short-lived upload token through OpenID Connect, so there is no long-lived secret to leak. Add the publisher on PyPI (project, owner, repo, workflow file release.yml, environment pypi) before the first tag.
+
Can I try it on TestPyPI first?
+
Yes. Register the trusted publisher on test.pypi.org and add repository-url: https://test.pypi.org/legacy/ under with: in the publish step.
+
What if the tag and pyproject.toml disagree?
+
The first step fails before anything is built or uploaded, with an error naming both values. A PyPI version cannot be reused once uploaded, so this check is worth keeping.
Pass the package's tag prefix and path to release.sh and to the notes step: bash "$RUNNER_TEMP/release.sh" --tag-prefix api/v --bump-file packages/api/package.json -- --path packages/api, then relnote --tag-prefix api/v --path packages/api --output …. Change the guard's 'v*' to 'api/v*'. See the monorepo guide for a matrix over packages.
Keep it if you like reviewing each release as a pull request, or need its per-language version updaters and manifest monorepo mode. This page is for repos where the open release PR is noise: it rebases on every merge, sits in the PR list, and needs release-please-config.json plus .release-please-manifest.json. Here the release happens when you press Run workflow, and nothing waits in between.
gh relnote create v1.4.0 # Release body from the same commits
Or let CI do it on the tag push with the relnote GitHub Action, or in GitLab CI and other CI. relnote sees that HEAD is the new tag and uses the tag before it, so the notes cover this release.