mrkeyoor.com_
Fri 07 Aug 22:57 UTC
PyPICLI & Toolingupdated 07 Aug 2026

twine

twine is the PyPA command line tool that uploads already-built Python distributions to PyPI or any other package index. It does one job and stops there: you build a wheel and sdist with something else, then twine check tells you whether the long description will render on the project page and twine upload pushes the files over a verified HTTPS connection. The separation is the point, because it means you upload the exact artifact you tested rather than whatever a build-and-upload command produced in the same breath. Credentials come from a .pypirc file, TWINE_ environment variables, the system keyring, or Trusted Publishing when it detects supported CI.

Verdict

The reference way to push a built distribution to PyPI, and the tool the official packaging tutorial still points at. If your project manager already has a publish command you probably do not need it, but there is nothing wrong with the one that does only this.

API stability4/5The core flags have looked the same for years and environment variable names have not moved, but the last two majors did remove behaviour: egg and wininst distributions, .tar.bz2, MD5 digests, third-party --skip-existing detection, and metadata version 2.0
Docs4/5twine.readthedocs.io documents every environment variable, proxy handling and the keyring pitfalls on headless Linux, with a towncrier changelog per release; the command reference is just generated help output, and Trusted Publishing is explained mainly through changelog entries
Maintenance5/5Maintained under the PyPA umbrella, 7.0.0 released 2026-07-27 and the repo pushed 2026-08-06, with 40 open issues on a 1.8k-star project and recent releases fixing real upload edge cases
Ecosystem4/58.5M weekly downloads and it is what the official packaging tutorial and the pypa/gh-action-pypi-publish action call; the deduction is that uv, hatch, poetry and flit now ship publish commands, so its share of new projects is shrinking

Use it if

  • You build with python -m build or setuptools and need something to do the actual upload, since neither of them publishes
  • You want to inspect or test the exact files before they go up, which build-then-upload-in-one-step tools make harder
  • You publish to a private index (Artifactory, Nexus, devpi, GitLab) and want one uploader that takes a repository URL and client certificates
  • You want to validate that your README renders on PyPI before release rather than after, using twine check
Skip it if

Setup reality

pip install twine is small and fast, and the friction shows up on the first upload instead. Authentication is token based in practice: username __token__ and the pypi- prefixed token as the password, which is why TWINE_USERNAME and TWINE_PASSWORD are the usual CI setup. The keyring dependency comes along for the ride and on a headless Linux box it can hang or throw where you expected a plain prompt, which is what keyring --disable exists for. Do a dry run against TestPyPI before the real thing, because PyPI will not let you re-upload a filename you have already published, not even after a delete. One recent trap: 7.0.0 fixed metadata version 2.5 uploads and in doing so stopped accepting metadata version 2.0, so artifacts from a very old build backend now get rejected at upload time.

Patterns

The normal release sequencebuild-and-upload

python -m build
twine check dist/*
twine upload dist/*

twine never builds; if dist is empty or stale you will upload nothing or the wrong version.

Rehearse on TestPyPI firsttest-pypi

twine upload -r testpypi dist/*

TestPyPI needs its own account and its own API token; the PyPI one will not authenticate there.

Check the long description renderscheck-readme

twine check --strict dist/*

--strict turns rendering warnings into a non-zero exit, which is what you want in CI; a README that fails here shows up as raw text on the project page.

Authenticate in CI with an API tokentoken-env

export TWINE_USERNAME=__token__
export TWINE_PASSWORD="pypi-AgEIcHlwaS5vcmc..."
twine upload --non-interactive dist/*

__token__ is a literal username, not a placeholder; --non-interactive turns a missing credential into an error instead of a prompt that hangs the job.

Store repositories in .pypircpypirc

[distutils]
index-servers =
    pypi
    internal

[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmc...

[internal]
repository = https://pkgs.example.com/simple/
username = ci
password = ${INTERNAL_TOKEN}

Lives at ~/.pypirc by default; twine upload -r internal selects a section, and 7.0.0 reads the file as UTF-8.

Upload to an index by URLprivate-index

twine upload \
  --repository-url https://pkgs.example.com/upload/ \
  --client-cert /etc/ssl/client.pem \
  dist/*

--repository-url overrides -r; --client-cert takes one file containing both the key and the certificate.

Re-run a partly failed uploadskip-existing

twine upload --skip-existing dist/*

Reliable on PyPI and TestPyPI only since 6.2.0 dropped the per-vendor detection hacks; on other indexes an already-uploaded file may still fail the run.

Upload PEP 740 attestations alongside filesattestations

twine upload --attestations dist/*

Passing the flag when no .publish.attestation files exist is a hard error, not a silent skip, so generate them first.

Publish from CI without a stored tokentrusted-publishing

# GitHub Actions job needs: permissions: id-token: write
twine upload dist/*

Since 6.1.0 twine can mint a short-lived PyPI token from ambient CI credentials, and 6.2.0 refreshes it during uploads that outlast the 15 minute validity window.

Upload one artifact rather than a globsingle-file

twine upload dist/mypkg-1.2.0-py3-none-any.whl

Useful when wheels are built on several runners; PyPI accepts additional files for a release that already exists, it just rejects a filename twice.

See why an upload was rejectedverbose-debug

twine upload --verbose --disable-progress-bar dist/*

--verbose prints the request and the index response, which is usually the only way to read the real error behind a 400.

Deal with the system keyringkeyring

keyring set https://upload.pypi.org/legacy/ __token__
# on a headless box where the backend misbehaves:
keyring --disable

keyring ships as a twine dependency, so it takes part in credential lookup whether or not you set it up.

Alternatives

PackageRegistryPick it when
uvPyPIYou already use uv for builds and want uv publish rather than a separate uploader
hatchPyPIYou want one tool covering environments, build and publish for the whole project lifecycle
flitPyPIYou maintain a simple pure Python package and want build plus publish in a single small command