# 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](https://pydevtools.com/handbook/explanation/what-is-pep-751.md) 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](https://pydevtools.com/handbook/explanation/why-use-trusted-publishing-for-pypi.md); attestations are cryptographically tied to the same OIDC identity that trusted publishing uses.

## Prerequisites

* A [PyPI](https://pypi.org) account with a [trusted publisher configured](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md) for your GitHub repository
* A GitHub repository containing a Python package with a [`pyproject.toml`](https://pydevtools.com/handbook/reference/pyproject.toml.md)

## Publish with the PyPA publish action

The [PyPA publish action](https://github.com/pypa/gh-action-pypi-publish) 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`:

```yaml
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/v1
```

The 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](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md) 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`](https://pypi.org/project/pypi-attestations/) CLI before publishing:

```yaml
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 publish
```

The `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:

1. Go to your package's page on [pypi.org](https://pypi.org).
2. Navigate to the release you just published.
3. 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](https://docs.pypi.org/api/integrity/) or the [`pypi-attestations`](https://pypi.org/project/pypi-attestations/) library.

## How this connects to pylock.toml

When a consumer runs a tool that generates a [pylock.toml](https://pydevtools.com/handbook/explanation/what-is-pep-751.md) lockfile, the tool queries PyPI for attestation data and records your package's publisher identity:

```toml
[[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](https://pydevtools.com/handbook/explanation/why-pylock-toml-includes-digital-attestations.md).

## Related

* [How to publish to PyPI with trusted publishing](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md)
* [Why pylock.toml includes digital attestations](https://pydevtools.com/handbook/explanation/why-pylock-toml-includes-digital-attestations.md)
* [Why use trusted publishing for PyPI?](https://pydevtools.com/handbook/explanation/why-use-trusted-publishing-for-pypi.md)
* [PyPI attestations documentation](https://docs.pypi.org/attestations/)
* [PEP 740: Index support for digital attestations](https://peps.python.org/pep-0740/)
