TWINE(3)

๐Ÿ“ฆ
version 6.2.0 ยท 3 versions
, in ubuntu stonking

๐Ÿ’– NAME ๐Ÿ’–

twine โ€” twine Documentation

Ok bestie, so this whole project follows the semantic versioning scheme (yes, the one at https://packaging.python.org/en/latest/discussions/versioning/#semantic-versioning-vs-calendar-versioning) plus the pre-release versioning scheme (https://packaging.python.org/en/latest/discussions/versioning/) โ€” both of which are the ones the Python Packaging Authority recommends. Periodt. ๐Ÿ’…


๐ŸŒธ TWINE 6.2.0 (2025-09-04) ๐ŸŒธ

โœจ Features โœจ

  • Automatically refresh short-lived PyPI token in long running Trusted Publishing uploads.

So like, if a trusted publishing upload job is taking longer than the validity period of a trusted publishing token (that's 15 minutes at the time of this writing, fyi), and we are already 10 minutes into that validity period, we will begin to attempt to replace the token on each subsequent request. Lowkey genius. (#1246 โ€” https://github.com/pypa/twine/issues/1246)

๐Ÿ› Bugfixes ๐Ÿ›

๐Ÿ’€ Deprecations and Removals ๐Ÿ’€

  • Removed the hacks that support --skip-existing for indexes other than PyPI and TestPyPI.

To date, these hacks continue to accrue and there have been numerous issues with them โ€” not the least of which being that every time we update them, the paid index providers change things to break the compatibility we implement for them. Beyond that, these hacks do not work when text is internationalized in the response from the index provider. It's giving maintenance nightmare.

For a sample of past issues, see:

(#1251 โ€” https://github.com/pypa/twine/issues/1251)

  • Removed support for MD5 digests during uploads.

This support was entirely vestigial, since MD5 is not a secure hash function and is not actually required on upload by PyPI. (Danger flag ๐Ÿšฉ she's not secure, bestie.)

Indices that cross-reference the uploaded content with a digest should use the provided SHA-256 and/or BLAKE2 digests instead. (#1262 โ€” https://github.com/pypa/twine/issues/1262)

๐ŸŒˆ Misc ๐ŸŒˆ


๐Ÿฆ‹ TWINE 6.1.0 (2025-01-17) ๐Ÿฆ‹

โœจ Features โœจ

๐Ÿ’€ Deprecations and Removals ๐Ÿ’€

๐ŸŒˆ Misc ๐ŸŒˆ

  • packaging is used instead of pkginfo for parsing and validating metadata. This aligns metadata validation to the one performed by PyPI. packaging version 24.0 or later is required. Support for metadata version 2.4 requires packaging 24.2 or later. pkginfo is not a dependency anymore. glow up โœจ (#1180 โ€” https://github.com/pypa/twine/issues/1180)
  • Use "source" instead of None as pyversion for sdist uploads. This is what PyPI (and most likely other package indexes) expects. (#1191 โ€” https://github.com/pypa/twine/issues/1191)

๐ŸŽ€ TWINE 6.0.1 (2024-11-30) ๐ŸŽ€

๐Ÿ› Bugfixes ๐Ÿ›

๐ŸŒˆ Misc ๐ŸŒˆ


๐ŸŒŸ TWINE 6.0.0 (2024-11-29) ๐ŸŒŸ

๐Ÿ› Bugfixes ๐Ÿ›

๐Ÿ’€ Deprecations and Removals ๐Ÿ’€

  • Username for PyPI and Test PyPI now defaults to __token__ but no longer overrides a username configured in the environment or supplied on the command line. Workflows still supplying anything other than __token__ for the username when uploading to PyPI or Test PyPI will now fail. So either supply __token__ or do not supply a username at all. No in-between, bestie. (#1121 โ€” https://github.com/pypa/twine/issues/1121)

๐ŸŒˆ Misc ๐ŸŒˆ


๐Ÿ“ TWINE 5.1.1 (2024-06-26) ๐Ÿ“

๐Ÿ› Bugfixes ๐Ÿ›

  • Resolved DeprecationWarnings when extracting twine metadata. (#1115 โ€” https://github.com/pypa/twine/issues/1115)
  • Fixed a bug for Repository URLs with auth where the port was lost. When attempting to prevent printing authentication credentials in URLs provided with username and password, we did not properly handle the case where the URL also contains a port (when reconstructing the URL). This is now handled and tested to ensure no regressions. no port left behind ๐Ÿ™ˆ (#fix-repo-urls-with-auth-and-port โ€” https://github.com/pypa/twine/issues/fix-repo-urls-with-auth-and-port)

๐Ÿ’œ TWINE 5.1.0 (2024-05-15) ๐Ÿ’œ

โœจ Features โœจ


๐Ÿ’œ TWINE 5.1.0 (2024-05-15) ๐Ÿ’œ

๐ŸŒˆ Misc ๐ŸŒˆ


๐Ÿ”ฅ TWINE 5.0.0 (2024-02-10) ๐Ÿ”ฅ

๐Ÿ› Bugfixes ๐Ÿ›

โœจ Features โœจ

๐ŸŒˆ Misc ๐ŸŒˆ


๐Ÿงš TWINE 4.0.2 (2022-11-30) ๐Ÿงš

๐Ÿ› Bugfixes ๐Ÿ›


๐ŸŒธ TWINE 4.0.1 (2022-06-01) ๐ŸŒธ

๐Ÿ› Bugfixes ๐Ÿ›


๐Ÿ‘‘ TWINE 4.0.0 (2022-03-31) ๐Ÿ‘‘

โœจ Features โœจ

๐Ÿ› Bugfixes ๐Ÿ›


โญ TWINE 3.8.0 (2022-02-02) โญ

โœจ Features โœจ

๐Ÿ› Bugfixes ๐Ÿ›


๐Ÿ“š TWINE 3.7.1 (2021-12-07) ๐Ÿ“š

โœจ Improved Documentation โœจ


๐ŸŒˆ TWINE 3.7.0 (2021-12-01) ๐ŸŒˆ

โœจ Features โœจ


๐Ÿ TWINE 3.6.0 (2021-11-10) ๐Ÿ

โœจ Features โœจ


๐Ÿ’– TWINE 3.5.0 (2021-11-02) ๐Ÿ’–

โœจ Features โœจ

๐Ÿ› Bugfixes ๐Ÿ›


๐ŸŽ‰ TWINE 3.4.2 (2021-07-20) ๐ŸŽ‰

๐Ÿ› Bugfixes ๐Ÿ›


๐ŸŒป TWINE 3.4.1 (2021-03-16) ๐ŸŒป

๐Ÿ› Bugfixes ๐Ÿ›


๐Ÿซถ TWINE 3.4.0 (2021-03-15) ๐Ÿซถ

โœจ Features โœจ

TWINE 3.3.0 (2020-12-23)

little bestie note: every #NNN floating around in here is an issue number over on the pypa/twine GitHub tracker ๐Ÿ’–

โœจ Features โœจ

  • Print the files that are about to be uploaded when you use upload --verbose (#670)
  • Print where the configuration file is living when you use upload --verbose (#675)
  • Print the source and the values of credentials when you use upload --verbose (#685)
  • Add support for Python 3.9 (#708)
  • Turn warnings into errors when you use check --strict (#715)

๐Ÿ› Bugfixes ๐Ÿ›

  • Make password optional when you use upload --client-cert (#678)
  • Support more Nexus versions with upload --skip-existing (#693)
  • Support Gitlab Enterprise with upload --skip-existing (#698)
  • Show a better error message for malformed files (#714)

๐Ÿ“š Improved Documentation ๐Ÿ“š

  • Adopt the PSF code of conduct (#680)
  • Adopt towncrier for the changelog (#718)

TWINE 3.2.0 (2020-06-24)

โœจ Features โœจ

  • Improve how HTTP errors are displayed during upload (#666)
  • Print packages and signatures to be uploaded when you use the --verbose option (#652)
  • Use red text when printing errors on the command line (#649)
  • Require the repository URL scheme to be http or https (#602)
  • Add type annotations, checked with mypy, with PEP 561 (peps.python.org/pep-0561/) support for users of Twine's API (#231)

๐Ÿ› Bugfixes ๐Ÿ›

  • Update the URL to the .pypirc specification (#655)
  • Don't raise an exception when the Python version can't be parsed from the filename (#612)
  • Fix an inaccurate retry message during upload (#611)
  • Clarify error messages for archive format (#601)

TWINE 3.1.1 (2019-11-27)

๐Ÿ› Bugfixes ๐Ÿ›

  • Restore --non-interactive as a flag that does not expect an argument. (#548)

TWINE 3.1.0 (2019-11-23)

โœจ Features โœจ

  • Add support for specifying --non-interactive as an environment variable. (#547)

TWINE 3.0.0 (2019-11-18)

โœจ Features โœจ

  • When a client certificate is indicated, all password processing is disabled. (#336)
  • Add the --non-interactive flag to abort the upload rather than interactively prompting if credentials are missing. (#489)
  • Twine now unconditionally requires the keyring library, and no longer supports uninstalling keyring as a means to disable that functionality. Instead, use keyring --disable keyring functionality if necessary. (#524)
  • Add Python 3.8 to classifiers. (#518)

๐Ÿ› Bugfixes ๐Ÿ›

  • More robust handling of the server response in --skip-existing (#332)

TWINE 2.0.0 (2019-09-24)

โœจ Features โœจ

  • Twine now requires Python 3.6 or later. Use pip 9 or pin to "twine<2" to install twine on older Python versions. (#437)

๐Ÿ› Bugfixes ๐Ÿ›

  • Require requests 2.20 or later to avoid reported security vulnerabilities in earlier releases. (#491)

TWINE 1.15.0 (2019-09-17)

โœจ Features โœจ

  • Improved output on the check command: it prints a message when there are no distributions given to check. Also improved handling of errors in a distribution's markup, so messages don't go bleeding through into the next distribution's errors. (#488)

TWINE 1.14.0 (2019-09-06)

โœจ Features โœจ

  • Show the Warehouse URL after uploading a package (#459)
  • Better error handling, plus a gpg2 fallback if gpg isn't available. (#456)
  • Now provides a more meaningful error on redirect during upload. (#310)

๐Ÿ› Bugfixes ๐Ÿ›

  • Fail more gracefully when encountering bad metadata (#341)

TWINE 1.13.0 (2019-02-13)

โœจ Features โœจ

  • Add the disable_progress_bar option to disable tqdm. (#427)
  • Allow defining an empty username and password in .pypirc. (#426)
  • Support keyring.get_credential. (#419)
  • Support keyring.get_username_and_password. (#418)
  • Add Python 3.7 to classifiers. (#416)

๐Ÿ› Bugfixes ๐Ÿ›

  • Restore prompts while retaining support for suppressing prompts. (#452)
  • Avoid requests-toolbelt to 0.9.0 to prevent attempting to use openssl when it isn't available. (#447)
  • Use io.StringIO instead of StringIO. (#444)
  • Only install pyblake2 if needed. (#441)
  • Use modern Python language features. (#436)
  • Specify python_requires in setup.py (#435)
  • Use https URLs everywhere. (#432)
  • Fix --skip-existing for Nexus Repos. (#428)
  • Remove unnecessary usage of readme_render.markdown. (#421)
  • Don't crash if there's no package description. (#412)
  • Fix keyring support. (#408)

๐ŸŒˆ Misc ๐ŸŒˆ

  • Refactor tox env and travis config. (#439)

TWINE 1.12.1 (2018-09-24)

๐Ÿ› Bugfixes ๐Ÿ›

  • Fix regression with upload exit code (#404)

TWINE 1.12.0 (2018-09-24)

โœจ Features โœจ

  • Add the twine check command to check long description (#395)
  • Drop support for Python 3.3 (#392)
  • Empower --skip-existing for Artifactory repositories (#363)

๐Ÿ› Bugfixes ๐Ÿ›

  • Avoid MD5 when Python is compiled in FIPS mode (#367)

TWINE 1.11.0 (2018-03-19)

โœจ Features โœจ

  • Remove PyPI as the default register package index. (#320)
  • Support Metadata 2.1 (PEP 566, peps.python.org/pep-0566/), including Markdown for description fields. (#319)

๐Ÿ› Bugfixes ๐Ÿ›

  • Raise an exception if attempting upload to deprecated legacy PyPI URLs. (#322)
  • Avoid uploading to PyPI when given an alternate repository URL, and require http:// or https:// in repository_url. (#269)

๐ŸŒˆ Misc ๐ŸŒˆ

  • Update PyPI URLs (packaging.python.org/guides/migrating-to-pypi-org/). (#318)
  • Add new maintainer, release checklists. (#314)
  • Add instructions on how to use keyring. (#277)

TWINE 1.10.0 (2018-03-07)

โœจ Features โœจ

  • Link to changelog from README (#46)
  • Reorganize & improve user & developer documentation. (#304)
  • Revise docs predicting the future of twine (#303)
  • Add architecture overview to docs (#296)
  • Add doc building instructions (#295)
  • Declare support for Python 3.6 (#257)
  • Improve progressbar (#256)

๐Ÿ› Bugfixes ๐Ÿ›

  • Degrade gracefully when keyring is unavailable (#315)
  • Fix changelog formatting (#299)
  • Fix syntax highlighting in README (#298)
  • Fix Read the Docs, tox, Travis configuration (#297)
  • Fix Travis CI and test configuration (#286)
  • Print progress to stdout, not stderr (#268)
  • Fix --repository[-url] help text (#265)
  • Remove obsolete registration guidance (#200)

TWINE 1.9.1 (2017-05-27)

๐Ÿ› Bugfixes ๐Ÿ›

  • Blacklist known bad versions of Requests. (#253)

TWINE 1.9.0 (2017-05-22)

๐Ÿ› Bugfixes ๐Ÿ›

  • Twine sends less information about the user's system in the User-Agent string. (#229)
  • Fix --skip-existing when used to upload a package for the first time. (#220)
  • Fix precedence of --repository-url over --repository. (#206)

๐ŸŒˆ Misc ๐ŸŒˆ

  • Twine will now resolve passwords using the keyring (pypi.org/project/keyring/) if available. The module can be required with the keyring extra.
  • Twine will use hashlib.blake2b on Python 3.6+ instead of pyblake2

TWINE 1.8.1 (2016-08-09)

๐ŸŒˆ Misc ๐ŸŒˆ

This helps people with https://upload.pypi.io still sitting in their .pypirc file.


๐ŸŒˆ TWINE 1.8.0 (2016-08-08) ๐ŸŒˆ

โœจ Features โœจ

  • okay so we switched from upload.pypi.io to upload.pypi.org, bestie. that's it that's the upgrade. (#201 โ€” https://github.com/pypa/twine/issues/201)
  • we now grab your config from the environment as a default, which is so handy. (#144 โ€” https://github.com/pypa/twine/issues/144)
    • the Repository URL will default to TWINE_REPOSITORY
    • the Username will default to TWINE_USERNAME
    • the Password will default to TWINE_PASSWORD
  • you can now hand over the Repository URL on the command-line with --repository-url, or sneak it in through an environment variable TWINE_REPOSITORY_URL. your choice, queen. (#166 โ€” https://github.com/pypa/twine/issues/166)
  • we generate Blake2b 256 digests for packages if pyblake2 is installed. want it? just run python -m pip install twine[with-blake2] and pyblake2 comes along with Twine. (#171 โ€” https://github.com/pypa/twine/issues/171)

๐Ÿ’… Misc ๐Ÿ’…

  • we now generate a SHA256 digest for all packages by default. slay.
  • we stopped testing on Python 2.6. it's giving moving on.
  • we warn you if you get a 500 error while uploading to *pypi.python.org (#199 โ€” https://github.com/pypa/twine/issues/199)

๐Ÿฆ‹ TWINE 1.7.4 (2016-07-09) ๐Ÿฆ‹

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ

  • corrected a packaging error. that's literally it. no cap.

๐Ÿ“ TWINE 1.7.3 (2016-07-08) ๐Ÿ“

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ

  • fixed uploads to instances of pypiserver that use --skip-existing. we were not properly checking the return status code on the response after attempting an upload. embarrassing, but we fixed it. (#195 โ€” https://github.com/pypa/twine/issues/195)

๐Ÿ’… Misc ๐Ÿ’…

  • we now avoid even attempting to upload a package if we can find it on Legacy PyPI. bye bestie.

๐ŸŒธ TWINE 1.7.2 (2016-07-05) ๐ŸŒธ

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐ŸŽ€ TWINE 1.7.1 (2016-07-05) ๐ŸŽ€

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐Ÿ‘‘ TWINE 1.7.0 (2016-07-04) ๐Ÿ‘‘

โœจ Features โœจ

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐Ÿ’œ TWINE 1.6.5 (2015-12-16) ๐Ÿ’œ

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐ŸŒˆ TWINE 1.6.4 (2015-10-27) ๐ŸŒˆ

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐Ÿฆ‹ TWINE 1.6.3 (2015-10-05) ๐Ÿฆ‹

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐Ÿ“ TWINE 1.6.2 (2015-09-28) ๐Ÿ“

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ

as part of the refactor for the 1.6.0 release, we were using the wrong name to find the signature file.

this also uncovered a bug where, if you're running twine in a situation where * is not expanded by your shell, we might also miss uploading signatures to PyPI. both of these got fixed as part of this.


๐ŸŒธ TWINE 1.6.1 (2015-09-18) ๐ŸŒธ

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐ŸŽ€ TWINE 1.6.0 (2015-09-14) ๐ŸŽ€

โœจ Features โœจ

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐Ÿ‘‘ TWINE 1.5.0 (2015-03-10) ๐Ÿ‘‘

โœจ Features โœจ

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ

๐Ÿ’… Misc ๐Ÿ’…

  • added a lower-limit to the requests dependency.

๐Ÿ’œ TWINE 1.4.0 (2014-12-12) ๐Ÿ’œ

โœจ Features โœจ

๐Ÿ™ˆ Bugfixes ๐Ÿ™ˆ


๐ŸŒˆ TWINE 1.3.0 (2014-03-31) ๐ŸŒˆ

โœจ Features โœจ

  • Additional functionality.

๐Ÿฆ‹ TWINE 1.2.2 (2013-10-03) ๐Ÿฆ‹

โœจ Features โœจ

  • Basic functionality.

we are happy you have decided to contribute to Twine. ๐Ÿ’–

please see the GitHub repository (https://github.com/pypa/twine) for code and more documentation, and the official Python Packaging User Guide (https://packaging.python.org/tutorials/packaging-projects/) for user documentation. to ask questions or get involved, you can join the Python Packaging Discourse forum (https://discuss.python.org/c/packaging/), #pypa or #pypa-dev on IRC (https://web.libera.chat/#pypa-dev,#pypa), or the distutils-sig mailing list (https://mail.python.org/mailman3/lists/distutils-sig.python.org/).

everyone interacting in the Twine project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the PSF Code of Conduct (https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md).


๐Ÿš€ GETTING STARTED ๐Ÿš€

okay so we use tox (https://tox.readthedocs.io/) to run tests, check code style, and build the documentation. to install tox, run:

python3 -m pip install tox

clone the twine repository from GitHub, then run:

cd /path/to/your/local/twine
tox -e dev

this creates a virtual environment (https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/), so that twine and its dependencies don't interfere with other packages installed on your machine. inside the virtual environment, twine is pointing at your local copy, so when you make changes you can easily see their effect. iconic behaviour honestly.

the virtual environment also contains the tools for running tests and checking code style, so you can run them on single files directly or in your code editor. however, we still encourage using the tox commands below on the whole codebase.

to use the virtual environment, run:

source venv/bin/activate

๐Ÿ“– Building the documentation ๐Ÿ“–

additions and edits to twine's documentation are welcome and appreciated. ๐Ÿซถ

to preview the docs while you're making changes, run:

tox -e watch-docs

then open a web browser to http://127.0.0.1:8000.

when you're done making changes, lint and build the docs locally before making a pull request. in your active virtual environment, run:

tox -e docs

the HTML of the docs will be written to docs/_build/html.

๐Ÿ’… Code style ๐Ÿ’…

to automatically reformat your changes with isort (https://timothycrosley.github.io/isort/) and black (https://black.readthedocs.io/), run:

tox -e format

to detect any remaining code smells with flake8 (https://flake8.pycqa.org/), run:

tox -e lint

to perform strict type-checking using mypy (https://mypy.readthedocs.io/), run:

tox -e types

any errors from lint or types need to be fixed manually. no shortcuts, bestie.

additionally, we prefer that import statements be used for packages and modules only, rather than individual classes or functions.

๐Ÿงช Testing ๐Ÿงช

we use pytest (https://docs.pytest.org/) for writing and running tests.

to run the tests in your virtual environment, run:

tox -e py

to pass options to pytest โ€” like, say, the name of a test โ€” run:

tox -e py -- tests/test_upload.py::test_exception_for_http_status

Twine is continuously tested against supported versions of Python using GitHub Actions (https://github.com/pypa/twine/actions). to run the tests against a specific version, e.g. Python 3.8, you'll need it installed on your machine. then, run:

tox -e py38

to run the "integration" tests of uploading to real package indexes, run:

tox -e integration

to run the tests against all supported Python versions, check code style, and build the documentation, run:

tox

๐Ÿ’– SUBMITTING CHANGES ๐Ÿ’–

okay bestie, you wanna slide your code into upstream? it's literally six steps, no cap:

  1. Fork the GitHub repository https://github.com/pypa/twine.
  2. Make a branch off of main and commit your changes to it.
  3. Run the tests, check code style, and build the docs as described above.
  4. Optionally, add your name to the end of the AUTHORS file using the format Name <email@domain.com> (url), where the (url) portion is optional.
  5. Submit a pull request to the main branch on GitHub, referencing an open issue.
  6. Add a changelog entry.

โœจ Changelog entries โœจ

The docs/changelog.rst file is built by towncrier (https://pypi.org/project/towncrier/) from files in the changelog/ directory. To add an entry, create a file in that directory named {number}.{type}.rst, where {number} is the pull request number, and {type} is feature, bugfix, doc, removal, or misc.

For example, if your PR number is 1234 and it's fixing a bug, then you would create changelog/1234.bugfix.rst. PRs can span multiple categories by creating multiple files: if you added a feature and deprecated/removed an old feature in PR #5678, you would create changelog/5678.feature.rst and changelog/5678.removal.rst.

A changelog entry is meant for end users and should only contain details relevant to them. In order to maintain a consistent style, please keep the entry to the point, in sentence case, shorter than 80 characters, and in an imperative tone. An entry should complete the sentence "This change will ...". If one line is not enough, use a summary line in an imperative tone, followed by a description of the change in one or more paragraphs, each wrapped at 80 characters and separated by blank lines.

You don't need to reference the pull request or issue number in a changelog entry, since towncrier will add a link using the number in the file name, and the pull request should reference an issue number. Similarly, you don't need to add your name to the entry, since that will be associated with the pull request.

Changelog entries are rendered using reStructuredText (https://www.writethedocs.org/guide/writing/reStructuredText/), but they should only have minimal formatting (such as monospaced text).


๐Ÿ›๏ธ ARCHITECTURAL OVERVIEW ๐Ÿ›๏ธ

Twine is a command-line tool for interacting with PyPI securely over HTTPS. Its three purposes are to be:

  1. A user-facing tool for publishing on pypi.org
  2. A user-facing tool for publishing on other Python package indexes (e.g., devpi instances)
  3. A useful API for other programs (e.g., zest.releaser) to call for publishing on any Python package index

Currently, twine has two principal functions: uploading new packages and registering new projects (https://packaging.python.org/glossary/#term-Project) (register is no longer supported on PyPI, and is in Twine for use with other package indexes).

Its command line arguments are parsed in twine/cli.py. The code for registering new projects is in twine/commands/register.py, and the code for uploading is in twine/commands/upload.py. The file twine/package.py contains a single class, PackageFile, which hashes the project files and extracts their metadata. The file twine/repository.py contains the Repository class, whose methods control the URL the package is uploaded to (which the user can specify either as a default, in the .pypirc file, or pass on the command line), and the methods that upload the package securely to a URL.

For more details, refer to the source documentation (currently a work in progress: https://github.com/pypa/twine/issues/635):

๐ŸŽ twine package

Top-level module for Twine.

The contents of this package are not a public API. For more details, see https://github.com/pypa/twine/issues/194 and https://github.com/pypa/twine/issues/665.

๐ŸŽ twine.commands package

Module containing the logic for the twine sub-commands.

The contents of this package are not a public API. For more details, see https://github.com/pypa/twine/issues/194 and https://github.com/pypa/twine/issues/665.

๐Ÿฉบ twine.commands.check module

Module containing the logic for twine check.

class twine.commands.check._WarningStream

write(text: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> int <https://docs.python.org/3/builtins/functions.html#int>

  • Write string to file.
  • Returns the number of characters written, which is always equal to the length of the string.

twine.commands.check._parse_content_type(value: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> Tuple <https://docs.python.org/3/library/typing.html#typing.Tuple>[str <https://docs.python.org/3/builtins/stdtypes.html#str>, Dict <https://docs.python.org/3/library/typing.html#typing.Dict>[str <https://docs.python.org/3/builtins/stdtypes.html#str>, str <https://docs.python.org/3/builtins/stdtypes.html#str>]]

Implement logic of deprecated cgi.parse_header().

From https://docs.python.org/3.11/library/cgi.html#cgi.parse_header.

twine.commands.check._check_file(filename: str <https://docs.python.org/3/builtins/stdtypes.html#str>, render_warning_stream: _WarningStream <#twine.commands.check._WarningStream>) -> Tuple <https://docs.python.org/3/library/typing.html#typing.Tuple>[List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>], bool <https://docs.python.org/3/builtins/functions.html#bool>]

Check given distribution.

twine.commands.check.check(dists: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>], strict: bool <https://docs.python.org/3/builtins/functions.html#bool> = False) -> bool <https://docs.python.org/3/builtins/functions.html#bool>

Check that a distribution will render correctly on PyPI and display the results.

This is currently only validates long_description, but more checks could be added.

Parameters

  • dists -- The distribution files to check.
  • output_stream -- The destination of the resulting output.
  • strict -- If True, treat warnings as errors.

Returns

True if there are rendering errors, otherwise False.

twine.commands.check.main(args: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> bool <https://docs.python.org/3/builtins/functions.html#bool>

Execute the check command.

Parameters

  • args -- The command-line arguments.

Returns

The exit status of the check command.

๐Ÿ“ twine.commands.register module

Module containing the logic for twine register.

twine.commands.register.register(register_settings: Settings <#twine.settings.Settings>, package: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> None <https://docs.python.org/3/builtins/constants.html#None>

Pre-register a package name with a repository before uploading a distribution.

Pre-registration is not supported on PyPI, so the register command is only necessary if you are using a different repository that requires it.

Parameters

  • register_settings -- The configured options relating to repository registration.
  • package -- The path of the distribution to use for package metadata.

Raises

twine.commands.register.main(args: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> None <https://docs.python.org/3/builtins/constants.html#None>

Execute the register command.

Parameters

  • args -- The command-line arguments.

๐Ÿš€ twine.commands.upload module

Module containing the logic for twine upload.

twine.commands.upload.skip_upload(response: Response <https://requests.readthedocs.io/en/latest/api.html#requests.Response>, skip_existing: bool <https://docs.python.org/3/builtins/functions.html#bool>, package: PackageFile <#twine.package.PackageFile>) -> bool <https://docs.python.org/3/builtins/functions.html#bool>

Determine if a failed upload is an error or can be safely ignored.

Parameters

  • response -- The response from attempting to upload package to a repository.
  • skip_existing -- If True, use the status and content of response to determine if the package already exists on the repository. If so, then a failed upload is safe to ignore.
  • package -- The package that was being uploaded.

Returns

True if a failed upload can be safely ignored, otherwise False.

twine.commands.upload._make_package(filename: str <https://docs.python.org/3/builtins/stdtypes.html#str>, signatures: Dict <https://docs.python.org/3/library/typing.html#typing.Dict>[str <https://docs.python.org/3/builtins/stdtypes.html#str>, str <https://docs.python.org/3/builtins/stdtypes.html#str>], attestations: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>], upload_settings: Settings <#twine.settings.Settings>) -> PackageFile <#twine.package.PackageFile>

Create and sign a package, based off of filename, signatures, and settings.

Additionally, any supplied attestations are attached to the package when the settings indicate to do so.

twine.commands.upload.upload(upload_settings: Settings <#twine.settings.Settings>, dists: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> None <https://docs.python.org/3/builtins/constants.html#None>

Upload one or more distributions to a repository, and display the progress.

If a package already exists on the repository, most repositories will return an error response. However, if upload_settings.skip_existing is True, a message will be displayed and any remaining distributions will be uploaded.

For known repositories (like PyPI), the web URLs of successfully uploaded packages will be displayed. obsessed โœจ

Parameters

  • upload_settings โ€” the configured options related to uploading to a repository.
  • dists โ€” the distribution files to upload to the repository. This can also include .asc and .attestation files, which will be added to their respective file uploads.

Raises


twine.commands.upload.main

twine.commands.upload.main(args: List <https://docs.python.org/3/library/typing.html#typing.List>[str <https://docs.python.org/3/builtins/stdtypes.html#str>]) -> None <https://docs.python.org/3/builtins/constants.html#None>

Execute the upload command. ๐Ÿ’…

Parameters

  • args โ€” the command-line arguments.

โœจ twine.auth module โœจ

Tokens expire after 15 minutes, so bestie let's start allowing renewal/replacement after 10 minutes โ€” that way if we fail, we may still have time to replace it before it expires. Thus, if our current time + this threshold is past the greater or equal to the expiration time, we should start trying to replace the token. the math is mathing ๐Ÿงฎ๐Ÿ’–

class twine.auth.CredentialInput

__init__(username: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None> = None, password: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None> = None) -> None <https://docs.python.org/3/builtins/constants.html#None>

class twine.auth.TrustedPublishingTokenRetrievalError

code: str <https://docs.python.org/3/builtins/stdtypes.html#str>
description: str <https://docs.python.org/3/builtins/stdtypes.html#str>

class twine.auth.TrustedPublishingToken

message: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>
errors: list <https://docs.python.org/3/builtins/stdtypes.html#list>[TrustedPublishingTokenRetrievalError <#twine.auth.TrustedPublishingTokenRetrievalError>] | None <https://docs.python.org/3/builtins/constants.html#None>
token: str <https://docs.python.org/3/builtins/stdtypes.html#str> | None <https://docs.python.org/3/builtins/constants.html#None>
success: bool <https://docs.python.org/3/builtins/functions.html#bool> | None <https://docs.python.org/3/builtins/constants.html#None>
expires: int <https://docs.python.org/3/builtins/functions.html#int> | None <https://docs.python.org/3/builtins/constants.html#None>

class twine.auth.TrustedPublishingAuthenticator

__init__(resolver: Resolver <#twine.auth.Resolver>) -> None <https://docs.python.org/3/builtins/constants.html#None>

class twine.auth.Resolver

class twine.auth.Private


โœจ twine.cli module โœจ


โœจ twine.exceptions module โœจ

Module containing exceptions raised by twine. the drama containment unit ๐Ÿ’€

  • exception twine.exceptions.TwineException โ€” base class for all exceptions raised by twine.
  • exception twine.exceptions.RedirectDetected โ€” a redirect was detected that the user needs to resolve.

In some cases, requests refuses to issue a new POST request after a redirect. In order to prevent a confusing user experience, we raise this exception to allow users to know the index they're uploading to is redirecting them. plot twist energy ๐ŸŒˆ

classmethod from_args(repository_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>, redirect_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> RedirectDetected <#twine.exceptions.RedirectDetected>
  • exception twine.exceptions.PackageNotFound โ€” a package file was provided that could not be found on the file system.

This is only used when attempting to register a package_file. girl where is it ๐Ÿ™ˆ

  • exception twine.exceptions.UploadToDeprecatedPyPIDetected โ€” an upload attempt was detected to deprecated PyPI domains.

The sites pypi.python.org and testpypi.python.org are deprecated. we don't do that anymore bestie โš ๏ธ

classmethod from_args(target_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>, default_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>, test_url: str <https://docs.python.org/3/builtins/stdtypes.html#str>) -> UploadToDeprecatedPyPIDetected <#twine.exceptions.UploadToDeprecatedPyPIDetected>

Return an UploadToDeprecatedPyPIDetected instance. ๐Ÿซถ

  • exception twine.exceptions.UnsupportedConfiguration โ€” an upload attempt was detected using features not supported by a repository.

The features specified either in configuration or on the command-line. lowkey a boundary โœ‹

class Builder

Build the parameters for an UnsupportedConfiguration exception.

In the event we add additional features we are not allowing with something other than PyPI or TestPyPI, we can use a builder to accumulate them all instead of requiring someone to run multiple times to discover all unsupported configuration options. efficiency queen ๐Ÿ‘‘

__init__() -> None <https://docs.python.org/3/builtins/constants.html#None>

okay so we're still deep in the Builder for UnsupportedConfiguration โ€” and bestie, there's literally three method calls left, we can do this ๐Ÿ’ชโœจ

  • with_repository_url(repository_url: str) -> Builder
  • with_feature(feature: str) -> Builder
  • finalize() -> UnsupportedConfiguration

no prose attached to these three, so we're keeping it exactly that shape and not inventing โœจvibesโœจ that aren't there ๐Ÿซก


and now: the exception buffet ๐Ÿ“ every single one of these lives under twine.exceptions, and they are, collectively, the "something went wrong bestie" energy of the whole package:

  • twine.exceptions.UnreachableRepositoryURLDetected โ€” an upload attempt got detected to a URL with no protocol prefix ๐Ÿšจ and FYI, all repository URLs must have a protocol (e.g., https://).
  • twine.exceptions.InvalidSigningConfiguration โ€” both the sign and identity parameters must be present.
  • twine.exceptions.InvalidSigningExecutable โ€” the signing executable must be installed on the system.
  • twine.exceptions.InvalidConfiguration โ€” raised when the configuration is invalid.
  • twine.exceptions.InvalidDistribution โ€” raised when a distribution is invalid.
  • twine.exceptions.NonInteractive โ€” raised in non-interactive mode when credentials could not be found.
  • twine.exceptions.TrustedPublishingFailure โ€” raised if we expected to use trusted publishing but couldn't.
  • twine.exceptions.InvalidPyPIUploadURL โ€” the repository configuration is trying to use PyPI with an incorrect URL. for example, https://pypi.org instead of https://upload.pypi.org/legacy. like. ma'am. that's not the door. ๐Ÿ™ˆ

๐ŸŒธ twine.package module ๐ŸŒธ

  • twine.package._safe_name(name: str) -> str โ€” convert an arbitrary string into a standard distribution name. any runs of non-alphanumeric/. characters get replaced with a single '-'. this one is copied from pkg_resources.safe_name for compatibility with warehouse, and there's a whole issue about it โ€” see https://github.com/pypa/twine/issues/743 ๐Ÿ’…

๐Ÿ‘‘ class twine.package.PackageMetadata

here's literally every single field this icon carries, no skips, no cuts ๐ŸŒˆ

  • metadata_version: str
  • name: str
  • version: str
  • platform: List[str]
  • summary: str
  • description: str
  • keywords: List[str]
  • home_page: str
  • author: str
  • author_email: str
  • license: str
  • supported_platform: List[str]
  • download_url: str
  • classifiers: List[str]
  • requires: List[str]
  • provides: List[str]
  • obsoletes: List[str]
  • maintainer: str
  • maintainer_email: str
  • requires_dist: List[str]
  • provides_dist: List[str]
  • obsoletes_dist: List[str]
  • requires_python: str
  • requires_external: List[str]
  • project_urls: Dict[str, str]
  • description_content_type: str
  • provides_extra: List[str]
  • dynamic: List[str]
  • license_expression: str
  • license_file: List[str]
  • comment: str
  • pyversion: str
  • filetype: str
  • gpg_signature: Tuple[str, bytes]
  • attestations: str
  • sha256_digest: str
  • blake2_256_digest: str

๐ŸŽ€ class twine.package.PackageFile

  • __init__(filename: str, comment: str | None, metadata: RawMetadata, python_version: str, filetype: str) -> None โ€” yes it takes all five, count them, they're all right there ๐Ÿซถ
  • version: str
  • gpg_signature: Tuple[str, bytes] | None
  • attestations: List[Dict[Any, str]] | None
  • classmethod from_filename(filename: str, comment: str | None) -> PackageFile
  • metadata_dictionary() -> PackageMetadata โ€” merges multiple sources of metadata into a single dictionary. it pulls values from the filename, PKG-INFO, hashers, and signature. so many little streams flowing into one river, obsessed ๐ŸŒŠ
  • add_attestations(attestations: List[str]) -> None
  • add_gpg_signature(signature_filepath: str, signature_filename: str) -> None
  • sign(sign_with: str, identity: str | None) -> None
  • classmethod run_gpg(gpg_args: Tuple[str, ...]) -> None

๐Ÿฆ‹ class twine.package.Hexdigest

  • Hexdigest(sha2, blake2) โ€” that's the shape of it, two slots, that's the whole thing
  • sha2: str | None โ€” alias for field number 0
  • blake2: str | None โ€” alias for field number 1
  • static __new__(_cls, sha2: str | None, blake2: str | None) โ€” create a new instance of Hexdigest(sha2, blake2)
  • _asdict() โ€” returns a brand new dict that maps the field names to their values, slay โœจ

and we are not done yet, so don't wander off, there's more of this page to get through ๐Ÿงš๐Ÿ’œ

  • _field_defaults = {} โ€” bestie it's literally just an empty dict, she contains nothing by default and that's her business ๐Ÿ’…
  • _fields = ('sha2', 'blake2') โ€” these are the two whole entire fields, in this exact order, no more no less ๐ŸŽ€
  • classmethod _make(iterable) โ€” makes you a fresh new Hexdigest object out of a sequence or any iterable, iconic factory behaviour โœจ
  • _replace(**kwds) โ€” hands you back a new Hexdigest object where the fields you named got swapped out for new values. it does not touch the original, she's immutable and she's thriving ๐Ÿฆ‹

class twine.package.HashManager

Manage our hashing objects for simplicity. ๐Ÿงš

This will also allow us to better test this logic. lowkey the whole reason she exists fr fr

  • __init__(filename: str) -> None โ€” Initialize our manager and hasher objects. she's setting up shop ๐ŸŒธ
  • _sha2_update(content: bytes) -> None โ€” feed the sha2 hasher some bytes, no return, no drama
  • _sha2_hexdigest() -> str | None โ€” hand back the sha2 hexdigest, or None if there isn't one yet
  • _blake_update(content: bytes) -> None โ€” same energy but for blake, feed her the bytes
  • _blake_hexdigest() -> str | None โ€” the blake hexdigest, str or None, that's the vibe check
  • hash() -> None โ€” Hash the file contents. just do the thing ๐Ÿ’ซ
  • hexdigest() -> Hexdigest โ€” Return the hexdigest for the file. yes she hands you the whole Hexdigest object, not a string, note that bestie ๐Ÿ“
heads up: upstream cross-links every single type name out to the python docs. I kept the types exactly as written and did not invent any, but you can go read up on str, bytes, bool and friends at docs.python.org if you want the lore. ๐Ÿ’œ

twine.repository module

class twine.repository.Repository

  • __init__(repository_url: str, username: str | None, password: str | None, disable_progress_bar: bool = False) -> None โ€” build a Repository. note disable_progress_bar defaults to False, and there are no defaults on repository_url, username or password, they're all required positional-style args here ๐Ÿซถ
  • close() -> None โ€” close her down, returns nothing
  • static _convert_metadata_to_list_of_tuples(data: PackageMetadata) -> List[Tuple[str, Any]] โ€” a static method that takes PackageMetadata and gives you back List[Tuple[str, Any]]. converting, bestie. that's the whole job ๐ŸŽ€
  • set_certificate_authority(cacert: str | None) -> None โ€” point her at a CA bundle path, or None, your call
  • set_client_certificate(clientcert: str | None) -> None โ€” same deal for the client cert, str or None
  • register(package: PackageFile) -> Response โ€” takes a PackageFile, gives you a Response
  • _upload(package: PackageFile) -> Response โ€” the underscored internal one, also PackageFile in, Response out
  • upload(package: PackageFile, max_redirects: int = 5) -> Response โ€” the public upload, and yes max_redirects defaults to 5 exactly, don't come for me ๐Ÿ”ฅ
  • package_is_uploaded(package: PackageFile, bypass_cache: bool = False) -> bool โ€” Determine if a package has been uploaded to PyPI already.
Warning: This does not support indexes other than PyPI or TestPyPI

Parameters

  • package (PackageFile) โ€” The package file that will otherwise be uploaded.
  • bypass_cache (bool) โ€” Force a request to PyPI.

Returns

True if package has already been uploaded, False otherwise

Return type

bool

  • release_urls(packages: List[PackageFile]) -> Set[str] โ€” feed her a List of PackageFiles, she gives you a Set of str. no cap, that's the signature ๐Ÿ’…
  • verify_package_integrity(package: PackageFile) -> None โ€” takes a PackageFile, returns None. she's checking, not returning, periodt โœ…

twine.settings module

Module containing logic for handling settings. ๐ŸŽ€

class twine.settings.Settings

Object that manages the configuration for Twine. ๐Ÿ‘‘

This object can only be instantiated with keyword arguments. okay so this part matters, listen up bestie:

For example,

Settings(True, username='fakeusername')

Will raise a TypeError. Instead, you would want

Settings(sign=True, username='fakeusername')

keyword args only, fr fr. no sneaking a positional in there ๐Ÿ™ˆ

__init__(*, attestations: bool = False, sign: bool = False, sign_with: str = 'gpg', identity: str | None = None, username: str | None = None, password: str | None = None, non_interactive: bool = False, comment: str | None = None, config_file: str = utils.DEFAULT_CONFIG_FILE, skip_existing: bool = False, cacert: str | None = None, client_cert: str | None = None, repository_name: str = 'pypi', repository_url: str | None = None, verbose: bool = False, disable_progress_bar: bool = False, **ignored_kwargs: Any) -> None

Initialize our settings instance. โœจ

Parameters

  • attestations โ€” Whether the package file should be uploaded with attestations.
  • sign โ€” Configure whether the package file should be signed.
  • sign_with โ€” The name of the executable used to sign the package with.
  • identity โ€” The GPG identity that should be used to sign the package file.
  • username โ€” The username used to authenticate to the repository (package index).
  • password โ€” The password used to authenticate to the repository (package index).
  • non_interactive โ€” Do not interactively prompt for username/password if the required credentials are missing.
  • comment โ€” The comment to include with each distribution file.
  • config_file โ€” The path to the configuration file to use.
  • skip_existing โ€” Specify whether twine should continue uploading files if one of them already exists. This primarily supports PyPI. Other package indexes may not be supported.
  • cacert โ€” The path to the bundle of certificates used to verify the TLS connection to the package index.
  • client_cert โ€” The path to the client certificate used to perform authentication to the index. This must be a single file that contains both the private key and the PEM-encoded certificate.
  • repository_name โ€” The name of the repository (package index) to interact with. This should correspond to a section in the config file.
  • repository_url โ€” The URL of the repository (package index) to interact with. This will override the settings inferred from repository_name.
  • verbose โ€” Show verbose output.
  • disable_progress_bar โ€” Disable the progress bar.
  • property username: str | None ๐ŸŒธ
  • property password: str | None</p> โ€” the password, as a property. No blurb in the source, so I'm not inventing one ๐Ÿ˜ค
  • _allow_noninteractive() -> AbstractContextManager[None] โ€” bypasses the NonInteractive error whenever a client cert is present. yes bestie, there's an escape hatch and it's this one ๐ŸŽ€
  • property verbose: bool</p> โ€” the verbose flag, but as a property.
  • static register_argparse_arguments(parser: ArgumentParser) -> None โ€” registers all the arguments for argparse. it does the boring wiring so you don't have to โœจ
  • classmethod from_argparse(args: Namespace) -> Settings</p> โ€” takes your parsed arguments and builds the actual Settings object out of them.
  • _handle_package_signing(sign: bool, sign_with: str, identity: str | None) -> None โ€” private helper. no docstring given, so no hot takes from me, periodt ๐Ÿ’…
  • _handle_repository_options(repository_name: str, repository_url: str | None) -> None</p> โ€” another private one, same deal.
  • _handle_certificates(cacert: str | None, client_cert: str | None) -> None โ€” private, handles the cert situation. the source stays mysterious ๐Ÿ™ˆ
  • verify_feature_capability() -> None</p> โ€” checks that the settings you configured are actually supported by the repository you configured.

Right now it checks exactly one thing:

  1. --skip-existing was only provided for PyPI and TestPyPI.

Raises twine.exceptions.UnsupportedConfiguration โ€” i.e. the features you configured just aren't available with the repository you pointed at. ๐Ÿ’€

  • check_repository_url() -> None โ€” makes sure you are not using legacy PyPI.

Raises twine.exceptions.UploadToDeprecatedPyPIDetected โ€” because your configured repository URL is for legacy PyPI. ๐Ÿšจ

  • create_repository() -> Repository</p> โ€” spins up a fresh repository object for uploading to. iconic ๐Ÿ’–

๐Ÿฆ‹ twine.utils module ๐Ÿฆ‹

๐Ÿ“ twine.utils.get_config

twine.utils.get_config(path: str) -> Dict[str, Dict[str, str | None]]

Reads your repository configuration out of a file (i.e. ~/.pypirc). ๐ŸŽ€

Format: https://packaging.python.org/specifications/pypirc/

If the default config file doesn't exist, it returns a default configuration for pypyi and testpypi. (yes, pypyi โ€” that's what it says, I'm not editing the source, bestie, I'm just the messenger ๐Ÿ˜ญ)

๐Ÿฆ‹ twine.utils.sanitize_url

twine.utils.sanitize_url(url: str) -> str

Sanitizes a URL. It strips out any user:password combos and swaps them for asterisks. If the string doesn't match the pattern, you just get the original URL back โ€” no notes. โœจ

Parameters

  • url โ€” str containing a URL to sanitize.

return:

str โ€” either sanitized or as entered, depending on whether it pattern-matched.

๐ŸŒธ twine.utils._validate_repository_url

twine.utils._validate_repository_url(repository_url: str) -> None

Validates the given URL for the allowed schemes and components. that's it, that's the whole job ๐Ÿ”ฅ

๐ŸŽ€ twine.utils.get_repository_from_config

twine.utils.get_repository_from_config(config_file: str, repository: str, repository_url: str | None = None) -> Dict[str, str | None]

Gets the repository config from the command-line values or from the .pypirc file. whichever one you handed it ๐Ÿ‘‘

๐Ÿ’œ twine.utils._config_from_repository_url

twine.utils._config_from_repository_url(url: str) -> Dict[str, str | None]

No blurb in the original. carrying on respectfully ๐Ÿซถ

โญ twine.utils.normalize_repository_url

twine.utils.normalize_repository_url(url: str) -> str

Also no blurb. the source gave us a signature and walked away ๐Ÿ’€

๐ŸŒˆ twine.utils.get_file_size

twine.utils.get_file_size(filename: str) -> str

Returns the size of a file in KB โ€” or in MB if it's >= 1024 KB. cute, it picks the nicer unit for you ๐Ÿ’…

๐Ÿฆ‹ twine.utils.check_status_code

twine.utils.check_status_code(response: Response, verbose: bool) -> None

Generates a genuinely helpful message based on what the repository sent back.

It raises a custom exception for errors it recognises. Otherwise it prints the response content โ€” and whether it prints that depends on the verbose option โ€” before re-raising the HTTPError. ๐Ÿ˜ค

๐Ÿ“ twine.utils.get_userpass_value

twine.utils.get_userpass_value(cli_value: str | None, config: Dict[str, str | None], key: str, prompt_strategy: Callable[[], str] | None = None) -> str | None

Pulls a credential (like a username or a password) out of the configuration.

It uses these rules, in order:

  1. If cli_value is specified, use that.
  2. If config[key] is specified, use that.
  3. If prompt_strategy is specified, use its return value.
  4. Otherwise return None.

Parameters

  • cli_value โ€” the value supplied from the command line.
  • config โ€” a dictionary of repository configuration values.
  • key โ€” the credential to look up in config, e.g. "username" or "password".
  • prompt_strategy โ€” an argumentless function to get the value, e.g. from keyring or by prompting the user.

Returns

The credential value, i.e. the username or password. ๐ŸŽ€

๐ŸŒธ twine.utils.get_cacert

twine.utils.get_cacert(cli_value: str | None, config: Dict[str, str | None], *, key: str = 'ca_cert', prompt_strategy: Callable[[], str] | None = None) -> str | None

Gets the CA bundle, via get_userpass_value(). lowkey just delegation and honestly? respect ๐Ÿ™Œ

๐Ÿ’œ twine.utils.get_clientcert

twine.utils.get_clientcert(cli_value: str | None, config: Dict[str, str | None], *, key: str = 'client_cert', prompt_strategy: Callable[[], str] | None = None) -> str | None

Gets the client certificate, also via get_userpass_value(). same energy, different key: 'client_cert'. โœจ

โญ twine.utils.make_requests_session

twine.utils.make_requests_session() -> Session

Prepares a requests Session with retries and twine's user-agent string. built different fr fr ๐Ÿ”ฅ

๐Ÿ‘‘ class twine.utils.EnvironmentDefault

Gets values from an environment variable. that's the entire description and she's serving it anyway ๐Ÿ’–

Okay so the __init__ we were literally mid-sentence on when the page cut off? here she is ๐Ÿซถ

__init__(env: str, required: bool = True, default: str | None = None, **kwargs: Any) -> None

Bestie, that's all she does. Moving on โœจ


class twine.utils.EnvironmentFlag

Set boolean flag from environment variable. That's the whole job. A vibe check, but for the environment ๐ŸŒˆ

__init__(env: str, **kwargs: Any) -> None
  • static bool_from_env(val: str | None) -> bool โ€” the way that this one lets '0' and 'false' and 'no' all count as False. Like, stringly-typed booleans, but make it cunty ๐Ÿ’…

๐Ÿ›ž twine.wheel module

class twine.wheel.Wheel

__init__(filename: str) -> None
  • property py_version: str โ€” a property! no parentheses, no call, just there ๐Ÿ‘‘
  • static find_candidate_metadata_files(names: List[str]) -> List[List[str]] โ€” Filter files that may be METADATA files. So it takes a list of names, and hands you back a list of lists of names. Yeah bestie, it's giving nested. ๐Ÿ˜ญ
  • read() -> bytes โ€” returns bytes, fr fr. Raw bytes, no decoding, no opinions.

๐Ÿ” Where Twine gets configuration and credentials

So a user can set the repository URL, username, and/or password via command line, .pypirc files, environment variables, and keyring. Four doors, one room. Pick whichever one is your main character energy ๐ŸŽ€


๐Ÿ’– ADDING A MAINTAINER

A checklist for adding a new maintainer to the project. Genuinely iconic behavior, growing the team ๐Ÿซถ

  1. Add them as a Member in the GitHub repo settings.
  2. Get them Test PyPI and canon PyPI usernames and add them as a Maintainer on our Test PyPI project <https://test.pypi.org/manage/project/twine/collaboration/> and canon PyPI <https://pypi.org/manage/project/twine/collaboration/>.
Both links, bestie. Both. Don't be lazy about the second one, that's the real one ๐Ÿ˜ญ

๐Ÿš€ MAKING A NEW RELEASE

A checklist for creating, testing, and distributing a new version. Buckle in, this is a whole ritual โœจ

  1. Choose a version number, and create a new branch
VERSION=3.4.2

git switch -c release-$VERSION
  1. Update docs/changelog.rst
tox -e changelog -- --version $VERSION

git commit -am "Update changelog for $VERSION"
  1. Open a pull request for review
  2. Merge the pull request, and ensure the GitHub Actions <https://github.com/pypa/twine/actions> build passes
  3. Create a new git tag for the version
git switch main

git pull --ff-only upstream main

git tag -m "Release v$VERSION" $VERSION
  1. Push to start the release, and watch it in GitHub Actions <https://github.com/pypa/twine/actions>
git push upstream $VERSION
  1. View the new release on PyPI <https://pypi.org/project/twine/>
Step 6 is the scary one. Pushing that tag is the point of no return, no takebacks, the release is releasing ๐Ÿ’€

๐Ÿ”ฎ FUTURE DEVELOPMENT

See our open issues <https://github.com/pypa/twine/issues>.

In the future, pip and twine may merge into a single tool; see ongoing discussion <https://github.com/pypa/packaging-problems/issues/60>. the way that this has been an ongoing discussion ๐Ÿ™ˆ

Twine is a utility for publishing <https://packaging.python.org/tutorials/packaging-projects/> Python packages to PyPI <https://pypi.org> and other repositories <https://packaging.python.org/glossary/#term-Package-Index>. It provides build system independent uploads of source and binary distribution artifacts <https://packaging.python.org/glossary/#term-Distribution-Package> for both new and existing projects <https://packaging.python.org/glossary/#term-Project>. Periodt. ๐Ÿ‘‘


๐Ÿค” WHY SHOULD I USE THIS?

The goal of Twine is to improve PyPI interaction by improving security and testability. That's it. That's the thesis.

The biggest reason to use Twine is that it securely authenticates you to PyPI over HTTPS using a verified connection, regardless of the underlying Python version. Meanwhile, python setup.py upload will only work correctly and securely if your build system, Python version, and underlying operating system are configured properly. Likeโ€ฆ that's a lot of ifs, bestie. That's so many ifs ๐Ÿ˜ญ

Secondly, Twine encourages you to build your distribution files. python setup.py upload only allows you to upload a package as a final step after building with distutils or setuptools, within the same command invocation. Which means you cannot test the exact file you're going to upload to PyPI to ensure that it works before uploading it. Not being able to test before you ship?? In this economy?? ๐Ÿ™ˆ

Finally, Twine allows you to pre-sign your files and pass the .asc files into the command line invocation:

twine upload myproject-1.0.1.tar.gz myproject-1.0.1.tar.gz.asc

This enables you to be assured that you're typing your gpg passphrase into gpg itself and not anything else, since you will be the one directly executing gpg --detach-sign -a <filename>. It's giving trust nobody, verify everything ๐Ÿฆ‹


๐ŸŒŸ FEATURES

  • Verified HTTPS connections ๐Ÿ”’
  • Uploading doesn't require executing setup.py
  • Uploading files that have already been created, allowing testing of distributions before release
  • Supports uploading any packaging format (including wheels <https://packaging.python.org/glossary/#term-Wheel>)

๐Ÿ“ฆ INSTALLATION

pip install twine

That's it. That's the install. So polite, so simple ๐ŸŽ€


๐ŸŽฏ USING TWINE

  1. Create some distributions in the normal way:
python -m build
  1. Upload to Test PyPI <https://packaging.python.org/guides/using-testpypi/> and verify things look right:
twine upload -r testpypi dist/*

Twine will prompt for your username and password.

  1. Upload to PyPI <https://pypi.org>:
twine upload dist/*
  1. Done! ๐ŸŽ‰
Note: Like many other command line tools, Twine does not show any characters when you enter your password. So no, it's not broken, you're not broken, it's just invisible. Keep typing bestie ๐Ÿ’… If you're using Windows and trying to paste your username, password, or token in the Command Prompt or PowerShell, Ctrl+V and Shift+Insert won't work. Instead, you can use "Edit > Paste" from the window menu, or enable "Use Ctrl+Shift+C/V as Copy/Paste" in "Properties". This is a known issue <https://bugs.python.org/issue37426> with Python's getpass module.

More documentation on using Twine to upload packages to PyPI is in the Python Packaging User Guide <https://packaging.python.org/tutorials/packaging-projects/>.

โœจ twine upload โœจ

Uploads one or more distributions to a repository. That's the whole vibe โ€” you built the thing, now you're yeeting it into the index ๐Ÿ“ฆโœจ

The synopsis, exactly as it lives:

usage: twine upload [-h] [-r REPOSITORY] [--repository-url REPOSITORY_URL]
                    [--attestations] [-s] [--sign-with SIGN_WITH]
                    [-i IDENTITY] [-u USERNAME] [-p PASSWORD]
                    [--non-interactive] [-c COMMENT]
                    [--config-file CONFIG_FILE] [--skip-existing]
                    [--cert path] [--client-cert path] [--verbose]
                    [--disable-progress-bar]
                    dist [dist ...]

what you're handing it (positional arguments):

  • dist โ€” the distribution files to upload to the repository (package index). Usually dist/*. Can also hold a .asc file to include an existing signature along with the file upload. Cute, right? ๐ŸŽ€

the flags, one by one, no skips:

  • -h, --help โ€” show this help message and exit. Classic. The "help me please" button ๐Ÿ™
  • -r, --repository REPOSITORY โ€” the repository (package index) to upload the package to. Should be a section in the config file [default: pypi]. (Can also be set via TWINE_REPOSITORY environment variable.)
  • --repository-url REPOSITORY_URL โ€” the repository (package index) URL to upload the package to. This overrides --repository. (Can also be set via TWINE_REPOSITORY_URL environment variable.) So like, this one wins the argument, periodt ๐Ÿ’…
  • --attestations โ€” upload each file's associated attestations.
  • -s, --sign โ€” sign files to upload using GPG.
  • --sign-with SIGN_WITH โ€” GPG program used to sign uploads [default: gpg].
  • -i, --identity IDENTITY โ€” GPG identity used to sign files.
  • -u, --username USERNAME โ€” the username to authenticate to the repository (package index) as. Has no effect on PyPI or TestPyPI. (Can also be set via TWINE_USERNAME environment variable.)
  • -p, --password PASSWORD โ€” the password to authenticate to the repository (package index) with. (Can also be set via TWINE_PASSWORD environment variable.)
  • --non-interactive โ€” do not interactively prompt for username/password if the required credentials are missing. (Can also be set via TWINE_NON_INTERACTIVE environment variable.)
  • -c, --comment COMMENT โ€” the comment to include with the distribution file.
  • --config-file CONFIG_FILE โ€” the .pypirc config file to use.
  • --skip-existing โ€” continue uploading files if one already exists. (Only valid when uploading to PyPI. Other implementations may not support this.) Bestie, read that parenthetical twice, it matters ๐Ÿซถ
  • --cert path โ€” path to alternate CA bundle (can also be set via TWINE_CERT environment variable).
  • --client-cert path โ€” path to SSL client certificate, a single file containing the private key and the certificate in PEM format.
  • --verbose โ€” show verbose output. Gimme the tea โ˜•
  • --disable-progress-bar โ€” disable the progress bar.

๐Ÿฆ‹ twine check ๐Ÿฆ‹

Checks whether your distribution's long description will render correctly on PyPI. Basically a vibe check for your README before it goes public ๐Ÿ˜ญ nobody wants to find out on the live page.

usage: twine check [-h] [--strict] dist [dist ...]

positional arguments:

  • dist โ€” the distribution files to check, usually dist/*

options:

  • -h, --help โ€” show this help message and exit
  • --strict โ€” fail on warnings. No notes taken, no passes given ๐Ÿ’€

๐ŸŒธ twine register ๐ŸŒธ

Pre-register a name with a repository before uploading a distribution.

Warning: pre-registration is not supported on PyPI โ€” https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata โ€” so the register command is only necessary if you are using a different repository that requires it. See issue #1627 on Warehouse โ€” https://github.com/pypi/warehouse/issues/1627 โ€” (the software running on PyPI) for more details.

So fr fr, if you're on PyPI you probably don't need this one at all. Lowkey deprecated energy, but it's still here for the repos that require it ๐Ÿ’–

usage: twine register [-h] [-r REPOSITORY] [--repository-url REPOSITORY_URL]
                      [--attestations] [-s] [--sign-with SIGN_WITH]
                      [-i IDENTITY] [-u USERNAME] [-p PASSWORD]
                      [--non-interactive] [-c COMMENT]
                      [--config-file CONFIG_FILE] [--skip-existing]
                      [--cert path] [--client-cert path] [--verbose]
                      [--disable-progress-bar]
                      package

register operation is not required with PyPI.org

positional arguments:

  • package โ€” file from which we read the package metadata.

options:

  • -h, --help โ€” show this help message and exit
  • -r, --repository REPOSITORY โ€” the repository (package index) to upload the package to. Should be a section in the config file [default: pypi]. (Can also be set via TWINE_REPOSITORY environment variable.)
  • --repository-url REPOSITORY_URL โ€” the repository (package index) URL to upload the package to. This overrides --repository. (Can also be set via TWINE_REPOSITORY_URL environment variable.)
  • --attestations โ€” upload each file's associated attestations.
  • -s, --sign โ€” sign files to upload using GPG.
  • --sign-with SIGN_WITH โ€” GPG program used to sign uploads [default: gpg].
  • -i, --identity IDENTITY โ€” GPG identity used to sign files.
  • -u, --username USERNAME โ€” the username to authenticate to the repository (package index) as. Has no effect on PyPI or TestPyPI. (Can also be set via TWINE_USERNAME environment variable.)
  • -p, --password PASSWORD โ€” the password to authenticate to the repository (package index) with. (Can also be set via TWINE_PASSWORD environment variable.)
  • --non-interactive โ€” do not interactively prompt for username/password if the required credentials are missing. (Can also be set via TWINE_NON_INTERACTIVE environment variable.)
  • -c, --comment COMMENT โ€” the comment to include with the distribution file.
  • --config-file CONFIG_FILE โ€” the .pypirc config file to use.
  • --skip-existing โ€” continue uploading files if one already exists. (Only valid when uploading to PyPI. Other implementations may not support this.)
  • --cert path โ€” path to alternate CA bundle (can also be set via TWINE_CERT environment variable).
  • --client-cert path โ€” path to SSL client certificate, a single file containing the private key and the certificate in PEM format.
  • --verbose โ€” show verbose output.
  • --disable-progress-bar โ€” disable the progress bar.

๐ŸŽ€ CONFIGURATION ๐ŸŽ€

Twine can read repository configuration from a .pypirc file โ€” either chillin' in your home directory, or handed over with the --config-file option. For details on writing and using .pypirc, see the specification in the Python Packaging User Guide: https://packaging.python.org/specifications/pypirc/ ๐Ÿ“–โœจ

๐ŸŒˆ Environment Variables ๐ŸŒˆ

Twine also supports configuration via environment variables. Options passed on the command line will take precedence over options set via environment variables. Definition via environment variable is helpful in environments where it is not convenient to create a .pypirc file (for example, on a CI/build server). Shoutout to every CI pipeline out there, y'all are the real ones ๐Ÿซถ

  • TWINE_USERNAME โ€” the username to use for authentication to the repository.
  • TWINE_PASSWORD โ€” the password to use for authentication to the repository.
  • TWINE_REPOSITORY โ€” the repository configuration, either defined as a section in .pypirc or provided as a full URL.
  • TWINE_REPOSITORY_URL โ€” the repository URL to use.
  • TWINE_CERT โ€” custom CA certificate to use for repositories with self-signed or untrusted certificates.
  • TWINE_NON_INTERACTIVE โ€” do not interactively prompt for username/password if the required credentials are missing.

๐Ÿ”ฅ Proxy Support ๐Ÿ”ฅ

Twine can be configured to use a proxy by setting environment variables. Like, for example, to use a proxy for just the twine command, without export-ing it for other tools:

HTTPS_PROXY=socks5://user:pass@host:port twine upload dist/*

For more information, see the Requests documentation on Proxies โ€” https://requests.readthedocs.io/en/latest/user/advanced.html#proxies โ€” and SOCKS โ€” https://requests.readthedocs.io/en/latest/user/advanced.html#socks โ€” plus an in-depth article about proxy environment variables: https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/ ๐Ÿงš๐Ÿ’œ

๐Ÿ”‘ KEYRING SUPPORT

Okay so bestie, imagine typing your password every single time you upload a distribution ๐Ÿ˜ญ absolutely not. Twine lets you store a username and password securely using keyring โ€” that's https://pypi.org/project/keyring/ for the girlies keeping receipts.

Keyring comes installed with Twine already ๐Ÿ’…, but on some systems (Linux mainly, of course) it may require additional installation steps: https://pypi.org/project/keyring/#installation-linux.

Once Twine is installed, you use the keyring program itself to set a username and password for each repository you might upload to โœจ

For example, to set an API token for PyPI:

keyring set https://upload.pypi.org/legacy/ __token__

โ€ฆand then paste your API key when prompted ๐ŸŽ€ that's it, that's the whole ritual.

For a different repository? Swap the URL out for the relevant repository URL, no biggie. Like for Test PyPI, use https://test.pypi.org/legacy/ ๐Ÿ’–

Note: if you're on Linux in a headless environment (like on a server, bestie, no desktop in sight), you'll need to do some additional steps to make sure Keyring can store secrets securely. See Using Keyring on headless systems at https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems ๐Ÿซถ

๐Ÿšซ Disabling Keyring

In most cases, simply not setting a password with keyring will let Twine fall back to just prompting you for a password. Easy.

But sometimes ๐ŸŒš the mere presence of Keyring causes unexpected or undesirable prompts from the backing system. Rude honestly. In those cases it may be desirable to disable Keyring altogether. To disable Keyring, run:

keyring --disable

See Twine issue #338 at https://github.com/pypa/twine/issues/338 for the discussion and background ๐Ÿ”ฅ


๐Ÿ‘‘ Author

Donald Stufft, Individual contributors

2019, Donald Stufft and individual contributors


And that's the whole page, bestie, we ate and left absolutely no crumbs ๐Ÿ’…๐Ÿ“ if you made it all the way to the end you're genuinely iconic. This was Twine version 6.2, dated Sep 10, 2026, and yeah it's TWINE(3) โ€” so next time someone asks you how to upload a distribution, you just point them here and say iykyk ๐ŸŒˆโญ bye pookie, go ship something ๐Ÿฆ‹๐Ÿ’œ

๐Ÿ“ฆ where this page comes from