Skip to main content

PyPiLibrary

Python PyPI template - companion to the .NET NuGetLibrary in this repo. Published to PyPI as ptr727-projecttemplate-library.

Stack

  • Build backend - hatchling via pyproject.toml
  • Env / deps / publish - uv (Astral)
  • Lint + format - ruff
  • Type checker - pyright
  • Tests - pytest
  • Publish - PyPI Trusted Publishing via pypa/gh-action-pypi-publish (no API token in repo secrets)
  • Version - Nerdbank.GitVersioning (NBGV) shared with the .NET side. CI replaces the __version__ line in _version.py (in place) before uv build. Branch-aware: on main the value is NBGV's AssemblyFileVersion (Major.Minor.Patch.BuildNumber, PEP 440 release); on develop it's Major.Minor.Patch.BuildNumber.dev0 (PEP 440 dev release - pip install filters the .dev suffix unless --pre is passed; the BuildNumber stays in the release segment so develop's segment grows past main's per commit and --pre actually prefers develop). Matches how NuGet/Docker tag develop builds as prerelease. All four artifact families (.NET assemblies, NuGet, Docker, PyPI) derive from the same NBGV computation per commit; only the formatting differs.

Layout

PyPiLibrary/
    pyproject.toml
    README.md
    src/
        ptr727_projecttemplate_library/
            __init__.py
            _version.py
            example.py
    tests/
        __init__.py
        test_example.py

Local Development

The repo's devcontainer installs uv automatically and runs uv sync for this project on first open. To work outside the devcontainer:

# from the repo root
cd PyPiLibrary
uv sync                          # creates .venv, installs deps + dev group
uv run ruff check                # lint
uv run ruff format --check       # formatting check
uv run pyright                   # type check
uv run pytest                    # tests
uv build                         # wheel + sdist into ./dist

Publishing

Releases are produced by .github/workflows/build-pypilibrary-task.yml (called from build-release-task.yml to build, lint, type-check, test, and upload the wheel + sdist as a workflow-run artifact). Publishing is a separate top-level publish-pypi job in publish-release.yml that downloads the artifact by name and runs Trusted Publishing - no PYPI_API_TOKEN secret is involved. The publish job has id-token: write only at that single job level, so the test-pull-request flow (which calls the same build task during PR validation) doesn't need to propagate that permission through the reusable workflow chain.

Two-channel publishing: pushes to both main and develop trigger publish-release.yml, and the "Compute PyPI version step" in build-pypilibrary-task.yml formats the version per branch:

  • main -> Major.Minor.Patch.BuildNumber (PEP 440 release). pip install ptr727-projecttemplate-library picks this up by default.
  • develop -> Major.Minor.Patch.BuildNumber.dev0 (PEP 440 dev release). The BuildNumber stays in the release segment so develop's release segment grows past main's per commit - that's what lets pip install --pre ptr727-projecttemplate-library actually resolve to a develop build (--pre would otherwise still pick the higher-on-release-segments main version). Same PyPI project; no separate "test" project required.

Edge case worth knowing: in the window between a release merge to main and the next commit on develop, develop's BuildNumber equals main's (or is one lower), so --pre will still resolve to the main release until a new develop commit lands. Self-healing.

This matches how NuGet (NBGV SemVer2 prerelease tags), Docker (NBGV SemVer2 image tags), and GitHub releases (softprops prerelease: true on develop) already mark develop builds.

First-time setup (one-time, on PyPI):

Prerequisite: enable 2FA on the PyPI account (TOTP or hardware key). PyPI requires it before any trusted publisher can be registered.

  1. PyPI -> Account settings -> Publishing -> Add a new pending publisher (direct link). If the project already exists on PyPI, go to the project page -> Manage -> Publishing -> Add a new publisher instead - the "pending" form is only for projects that don't exist yet. Fields:
    • PyPI project name: ptr727-projecttemplate-library
    • Owner: ptr727
    • Repository name: ProjectTemplate
    • Workflow filename: publish-release.yml
    • Environment name: pypi
  2. GitHub repo -> Settings -> Environments -> New environment -> pypi. The environment owns deploy-time guardrails:
    • Deployment branch rule -> Selected branches and tags -> add both main (release channel) and develop (prerelease channel). This step is mandatory - Trusted Publishing without a branch restriction is a documented security anti-pattern. Any other branch (feature branches, codegen, etc.) is blocked at the env gate even if a workflow misconfiguration ever tried to publish from it.
    • (Optional) add yourself as a required reviewer so each publish requires a click - useful belt-and-suspenders against an accidental release.
  3. The first successful release converts the pending publisher to a real publisher. After that the same OIDC exchange validates against the real publisher on every release.

Troubleshooting:

  • invalid-publisher: ... Publisher with matching claims was not found - the publisher hasn't been registered yet, or one of the five claim fields (owner, repo, workflow filename, environment name, project name) doesn't match. Re-check step 1.
  • manifest unknown from docker: pulling ghcr.io/pypa/gh-action-pypi-publish - the SHA pinned in publish-release.yml doesn't correspond to a release tag with a published GHCR image. Pin to the SHA that the upstream tag (# vX.Y.Z comment) actually points at on pypa/gh-action-pypi-publish.

Fallback (API token instead of Trusted Publishing): drop the id-token: write permission from the publish-pypi job, add password: ${{ secrets.PYPI_API_TOKEN }} to the pypa/gh-action-pypi-publish step, and store the token as a repo secret. Also pass attestations: false since attestations require the OIDC token. The OIDC path is preferred - no long-lived secret in the repo - so use the token method only when Trusted Publishing isn't an option.

Template Adoption

When deriving a new project from this template:

  • Replace the package name ptr727-projecttemplate-library (in pyproject.toml, this README, and CI) with your name.

  • Rename src/ptr727_projecttemplate_library/ to your import name.

  • Re-register the trusted publisher on PyPI under the new project name.

  • Pick a versioning scheme. The template defaults to NBGV-driven versioning shared with the .NET side: _version.py holds __version__ = "0.0.0" as a local-development placeholder, and the CI steps "Compute PyPI version step" + "Write version into _version.py step" in build-pypilibrary-task.yml compute and rewrite the value before uv build. The version is branch-aware: main pushes ship M.N.P.B (PEP 440 release), develop pushes ship M.N.P.B.dev0 (PEP 440 dev release - same release segment as main, .dev0 marks it as prerelease so pip install filters it unless --pre is passed). The BuildNumber stays in the release segment so develop's segment grows past main's per commit, which is what lets --pre actually prefer develop. On main the PyPI version equals the .NET FileVersion stamp exactly; on develop it equals the same FileVersion numerically but with a trailing .dev0. .NET's AssemblyVersion (a separate NBGV output) and NuGet/Docker (NBGV SemVer2) carry different strings across artifact families on both channels; all four derive from the same NBGV computation against version.json + git history per commit. If you want a different scheme, replace both _version.py and the workflow steps. Two common alternatives:

    • hatch-vcs - derive the version from git tags. Add it to [build-system].requires and switch [tool.hatch.version] to source = "vcs". Drop the CI overwrite step. Pairs well with tag-driven releases and removes the NBGV dependency.
    • Manual bumps - edit _version.py in each release PR. Simplest, but easy to forget. Drop the CI overwrite step.

    The publish workflow uses skip-existing: true so a re-upload of the same version is a no-op instead of a failure - useful when iterating on releases without bumping NBGV.

If you don't want a Python project at all, delete the PyPiLibrary/ folder, the build-pypilibrary-task.yml workflow, the build-pypilibrary job in build-release-task.yml, the publish-pypi job in publish-release.yml, and the uv block in .github/dependabot.yml.

Metadata

Release files for ptr727-projecttemplate-library 1.0.114.20500

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ptr727-projecttemplate-library 1.0.114.20500
File Size Uploaded
ptr727_projecttemplate_library-1.0.114.20500.tar.gz 6.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ptr727-projecttemplate-library 1.0.114.20500
File Interpreter ABI Platform
ptr727_projecttemplate_library-1.0.114.20500-py3-none-any.whl Python 3 none any Details

Total release size: 12.5 kB

Release files / ptr727_projecttemplate_library-1.0.114.20500.tar.gz

Download URL ptr727_projecttemplate_library-1.0.114.20500.tar.gz
Size 6.2 kB
Tags Source
SHA-256 checksum
How to use checksums
5b646e86f96f96254b9489f23e94202194357f548a5e1d91fab7b2dcd114673e
BLAKE2b-256 checksum
How to use checksums
6bc6c03eda3541a69cbdabdefaf28e0b23a8c4ef2a735ea86a7a5f55940a8258
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 29, 2026.

Transparency log

Release files / ptr727_projecttemplate_library-1.0.114.20500-py3-none-any.whl

Download URL ptr727_projecttemplate_library-1.0.114.20500-py3-none-any.whl
Size 6.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cf3b662fdf259ecd9a10ca3646a35f15097c50510c92f9d58e890f405985d8d8
BLAKE2b-256 checksum
How to use checksums
0b2790011412461479ab927bc5d45b01e41db6987e28eb8bb9456e3a70420fd7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 29, 2026.

Transparency log

Release history Release notifications | RSS feed

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page