How to Publish Python Packages with Digital Attestations
Digital attestations let PyPI record who published each file, not just what the file contains. When consumers generate a pylock.toml lockfile, their tools can write your package’s publisher identity into the lockfile. Any change to that identity in a future update becomes visible in code review.
This guide shows how to configure a GitHub Actions workflow that publishes to PyPI with attestations enabled. The setup requires trusted publishing; attestations are cryptographically tied to the same OIDC identity that trusted publishing uses.
Prerequisites
- A PyPI account with a trusted publisher configured for your GitHub repository
- A GitHub repository containing a Python package with a
pyproject.toml
Publish with the PyPA publish action
The PyPA publish action generates and uploads PEP 740 attestations by default when used with trusted publishing. No extra flags or configuration are required.
Create .github/workflows/publish.yml:
name: Publish to PyPI
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- run: uv build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
permissions:
id-token: write
environment:
name: pypi
url: https://pypi.org/p/<YOUR_PACKAGE_NAME>
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1The action handles both the upload and attestation generation in a single step. It signs each distribution file using Sigstore with the OIDC identity from your GitHub Actions workflow, then uploads the signed attestations to PyPI alongside the packages.
Note
Build and publish are split into separate jobs so the publish job has only the id-token: write permission it needs. This follows the principle of least privilege.
What about uv publish?
The existing trusted publishing how-to shows how to use uv publish with OIDC authentication. While uv publish handles trusted publishing, it does not generate PEP 740 attestations on its own. If attestation files (named <distribution>.publish.attestation) are present in the dist/ directory, uv publish will upload them alongside the packages.
You can generate those files with the pypi-attestations CLI before publishing:
publish:
needs: build
runs-on: ubuntu-latest
permissions:
id-token: write
environment:
name: pypi
url: https://pypi.org/p/<YOUR_PACKAGE_NAME>
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: astral-sh/setup-uv@v7
- run: uv tool run pypi-attestations sign dist/*
- run: uv publishThe pypi-attestations sign command uses Sigstore ambient credentials from the GitHub Actions OIDC token to generate .publish.attestation files next to each distribution. uv publish then discovers and uploads them automatically.
For most projects, the PyPA publish action shown above is the simpler path since it handles attestation generation internally. The uv publish approach is useful if you need more control over the signing step or are integrating with other tooling.
Tip
If you use uv publish with a private index that does not support attestations, pass --no-attestations to prevent upload failures.
Verify attestations on PyPI
After publishing, check that attestations are present:
- Go to your package’s page on pypi.org.
- Navigate to the release you just published.
- Each distribution file should show a Provenance badge linking to the GitHub Actions workflow that produced it.
You can also verify programmatically using the PyPI Integrity API or the pypi-attestations library.
How this connects to pylock.toml
When a consumer runs a tool that generates a pylock.toml lockfile, the tool queries PyPI for attestation data and records your package’s publisher identity:
[[packages]]
name = "your-package"
version = "1.0.0"
[[packages.attestation-identities]]
kind = "GitHub"
repository = "your-org/your-package"
workflow = "publish.yml"
environment = "pypi"If an attacker later uploads a malicious version without going through your CI pipeline, the attestation identity will either be missing or different. That change shows up in the consumer’s lockfile diff, making the compromise visible during code review.