Publishing with Poetry

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the Python Reference bibliography.

Poetry builds and uploads in one tool: poetry build makes the sdist and wheel, poetry publish uploads the current version’s files to PyPI or to any index that implements the legacy upload API. This page publishes the Poetry-flavoured birdtally to the same local pypiserver used on Publishing with build & twine (localhost:8084, uploads require HTTP basic authentication), with Poetry 2.5.1 on 2026-10-22, on Linux with CPython 3.13.16 as the project’s interpreter (hence the py3.13 virtualenvs; Poetry itself running on 3.14.6). TestPyPI and PyPI are not used. Repositories and credentials in general are on Repositories & Package Sources.

Configure the target

Upload targets are per-user configuration, not part of pyproject.toml:

poetry config repositories.local http://localhost:8084
poetry config repositories.local
export POETRY_HTTP_BASIC_LOCAL_USERNAME=birdwatch
export POETRY_HTTP_BASIC_LOCAL_PASSWORD=…        # from your secret store
{'url': 'http://localhost:8084'}
  • The URL is the upload (legacy API) endpoint, not the /simple/ one. Since 2.4 Poetry adds a missing trailing slash, so http://localhost:8084 works.

  • Credentials: poetry config http-basic.local <user> (prompts for the password; keyring or auth.toml), poetry config pypi-token.<name> <token> for token-based indexes, or the environment variables POETRY_HTTP_BASIC_<NAME>_USERNAME/_PASSWORD and POETRY_PYPI_TOKEN_<NAME> in CI. pypi and testpypi use pypi-token.pypi / pypi-token.testpypi.

  • Where a system keyring is available, http-basic stores the password in it (as poetry-repository-<name>, for example poetry-repository-pypi) and only the username in auth.toml; if the keyring fails or is unsupported, the password goes into auth.toml in plain text. poetry config keyring.enabled false (POETRY_KEYRING_ENABLED) turns the keyring off on purpose, for example on a CI runner without one, where the environment variables are the better choice anyway.

  • A password that starts with - needs poetry config — http-basic.local user -secret; an empty password (some hosted indexes, such as Gemfury, use the token as the username) is poetry config http-basic.local <token> "".

  • Since 2.5.0 Poetry never sends credentials configured for an https repository over http.

Publish

With an old 0.1.0 build still in dist/ and the version bumped to 0.1.2:

poetry version 0.1.2
poetry publish -n -r local --build --dry-run
Bumping version from 0.1.0 to 0.1.2
Warning: There are 2 files ready for publishing in dist. Build anyway!
Creating virtualenv birdtally-CYLjbS5k-py3.13 in …/virtualenvs
Building birdtally (0.1.2)
Building sdist
  - Building sdist
  - Built birdtally-0.1.2.tar.gz
Building wheel
  - Building wheel
  - Built birdtally-0.1.2-py3-none-any.whl

Publishing birdtally (0.1.2) to local
 - Uploading birdtally-0.1.2.tar.gz 0%
 - Uploading birdtally-0.1.2.tar.gz 100%
 - Uploading birdtally-0.1.2-py3-none-any.whl 0%
 - Uploading birdtally-0.1.2-py3-none-any.whl 100%

Three details:

  • --build builds first. If dist/ already holds files, Poetry asks There are 2 files ready for publishing in dist. Build anyway? (yes/no) [no]; without a terminal that prompt repeats forever, so pass -n (then it prints the warning above and builds) or clean dist/ yourself.

  • Only the files of the current version are uploaded: the 0.1.0 files were left alone. Since 2.4.0 a failed --build stops the publish instead of uploading stale files.

  • --dry-run does everything except the upload, progress lines included. Without it, the same command uploaded both files, and the local index then listed birdtally-0.1.2-py3-none-any.whl and birdtally-0.1.2.tar.gz next to the earlier releases.

Publishing again fails, here with pypiserver’s 409 Conflict (twine got a 400 for the same situation):

Publishing birdtally (0.1.2) to local
 - Uploading birdtally-0.1.2.tar.gz 0%
 - Uploading birdtally-0.1.2.tar.gz 100%

HTTP Error 409: Conflict | b'\n    <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">…<pre>Package &#039;birdtally-0.1.2.tar.gz&#039; already exists!…'

Unlike twine, Poetry’s --skip-existing works against this index:

 - Uploading birdtally-0.1.2.tar.gz 0%
 - Uploading birdtally-0.1.2.tar.gz 100%
 - Uploading birdtally-0.1.2.tar.gz File exists. Skipping
 - Uploading birdtally-0.1.2-py3-none-any.whl 0%
 - Uploading birdtally-0.1.2-py3-none-any.whl 100%
 - Uploading birdtally-0.1.2-py3-none-any.whl File exists. Skipping

A wrong password ends in HTTP Error 403: Forbidden, exit code 1.

poetry publish options

Option Meaning

-r, --repository

The configured repository (default: PyPI).

-u, --username, -p, --password

Credentials on the command line; prefer configuration or environment variables.

--cert, --client-cert

A CA bundle and a client certificate (or set certificates.<name>.cert / .client-cert).

--dist-dir

Where the files are (default dist, 1.8).

--build

Build before publishing.

--dry-run

Do everything but upload.

--skip-existing

Ignore files the repository already has.

Source: Poetry CLI: publish (options).

Certificates and mutual TLS

A private index behind a company certificate authority, or one that requires a client certificate (mutual TLS), is configured per repository name. The certificate settings live with the credentials, in auth.toml in Poetry’s configuration directory, not in config.toml:

poetry config repositories.internal https://pypi.internal.example.org/legacy/
poetry config certificates.internal.cert /etc/ssl/internal-ca.pem
poetry config certificates.internal.client-cert /etc/ssl/birdtally-client.pem
poetry config certificates.internal.cert
cat "$POETRY_CONFIG_DIR/auth.toml"
POETRY_CERTIFICATES_INTERNAL_CERT=/tmp/other-ca.pem poetry config --list | grep certificates
There is no certificates.internal.cert setting.
[certificates.internal]
cert = "/etc/ssl/internal-ca.pem"
client-cert = "/etc/ssl/birdtally-client.pem"
certificates.internal.cert = "/etc/ssl/internal-ca.pem"  # /tmp/other-ca.pem
certificates.internal.client-cert = "/etc/ssl/birdtally-client.pem"
  • certificates.<name>.cert is the CA bundle used to verify the repository’s server certificate; certificates.<name>.client-cert is the client certificate Poetry presents. Either can be set without the other.

  • The environment forms are POETRY_CERTIFICATES_<NAME>_CERT and POETRY_CERTIFICATES_<NAME>_CLIENT_CERT. As with other settings, --list shows the stored value and, after the #, the value the variable overrides it with.

  • --list shows the certificate settings, but reading one by name fails with There is no … setting. (exit code 1) in Poetry 2.5.1; use --list to check them.

  • poetry publish --cert and --client-cert give the same two files for a single run.

  • certificates.<name>.cert false skips TLS verification for that repository. The documentation warns against it; use it only for a throwaway index with a self-signed certificate, never for one you upload releases to.

The commands above only write and read the configuration (in a scratch POETRY_CONFIG_DIR); no upload to a TLS or mutual-TLS index was run here. The host pypi.internal.example.org and the certificate paths are placeholders.

TestPyPI and PyPI (not run here)

poetry config repositories.testpypi https://test.pypi.org/legacy/
export POETRY_PYPI_TOKEN_TESTPYPI="$TESTPYPI_TOKEN"
poetry publish -n -r testpypi --build

export POETRY_PYPI_TOKEN_PYPI="$PYPI_TOKEN"
poetry publish -n --build

pypi needs no repositories entry. Poetry uses the token as the password with the __token__ username.

No Trusted Publishing in Poetry

Poetry has no built-in support for PyPI Trusted Publishing or PEP 740 attestations: the request, python-poetry/poetry#7940, has been open since 2023-05-17. The documented way to combine them is to build with Poetry and upload with the PyPA action, which handles the OIDC exchange and the attestations:

- run: pipx install poetry==2.5.1
- run: poetry build
- uses: pypa/gh-action-pypi-publish@release/v1      # in a job with id-token: write

The complete workflow is on GitHub Actions & GitLab CI Release Workflows.

Compared with build & twine

Poetry build + twine

Build

poetry build (or --build)

python -m build

Target

poetry config repositories.<name>

.pypirc or --repository-url

Credentials

keyring, auth.toml, POETRY_*

keyring, .pypirc, TWINE_*

Check before upload

poetry check (metadata only)

twine check --strict (renders the README)

--skip-existing

any index

PyPI only

Trusted Publishing, attestations

no

twine 6.1+ (preliminary), the PyPA action

twine check also works on Poetry’s dist/: run it before poetry publish.