šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Publishing Python Packages to PyPI

Publishing a package to PyPI correctly — TestPyPI, API tokens over passwords, and versioning discipline, so a mistaken publish doesn't become a permanent, unfixable problem.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

If version 1.0.0 is published to PyPI with a critical bug, and you delete that release, can you later re-upload a FIXED version also numbered 1.0.0?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Building a package is reversible — you can rebuild it as many times as you want locally. Publishing to PyPI is not: a published version number can never be reused, even if you delete the release. This lesson covers publishing correctly the first time, including the safety net of testing on TestPyPI first.

1Publishing Is Permanent: The Consequence of a Version Number

twine upload dist/* (or the equivalent uv publish/poetry publish covered in earlier lessons) is the step that actually sends your built wheel and sdist to PyPI, making them publicly installable via pip install your-package. This is a genuinely different kind of action than every other step in the packaging pipeline covered so far: building, even publishing to a private or internal index, is reversible or at least contained — publishing to the real, public PyPI is not.

The specific, easy-to-miss policy that makes this consequential: PyPI permanently reserves a version number once it has been uploaded, even if you later delete that specific release. Publishing 1.0.0 with a critical bug, realizing the mistake, and deleting the release does *not* free up 1.0.0 for a corrected re-upload — the actual fix must be published under a genuinely new version number (1.0.1, or 1.0.0.post1), permanently, with no way to reuse the original number.

This policy exists specifically to preserve reproducibility across the entire ecosystem: if version numbers *could* be reused, pip install package==1.0.0 run by two different people, at two different times, could silently install two genuinely different sets of code — exactly the reproducibility guarantee this curriculum's Dependency Management lesson emphasized, which PyPI's immutable-version-number policy protects at the ecosystem level, not just within one project's own lock file.

āœ•
—
+
$ python -m build
$ twine upload dist/*
# Uploads dist/my_package-1.0.0.whl and dist/my_package-1.0.0.tar.gz to PyPI
# Once uploaded, version 1.0.0 can NEVER be re-uploaded, even if deleted
localhost:3000
Irreversible Consequence
Once uploaded, a version number is permanent
Deleting a release does NOT free it for reuse

2TestPyPI: A Genuine Practice Run With No Permanent Consequence

Given that publishing is irreversible in this specific, important sense, TestPyPI (https://test.pypi.org) exists as a genuinely separate, fully-functional instance of the same PyPI infrastructure, specifically for practicing the entire publish workflow — uploading, then verifying via pip install --index-url https://test.pypi.org/simple/ your-package that the published package actually installs correctly and works as expected — without touching, or consuming a version number on, the real, permanent PyPI at all.

This is not a simulation or a mock — it's the actual same publish-and-install mechanics running against a genuinely separate registry, meaning a successful TestPyPI publish-and-install cycle is real, meaningful evidence that your pyproject.toml metadata, build configuration, and package discovery (from the Building Packages lesson) are all correctly configured, *before* you commit to an irreversible action on the real registry.

The professional workflow this establishes for any package's first release, or any release involving meaningful configuration changes (a new dependency, a build backend switch): publish to TestPyPI first, install from TestPyPI and verify it genuinely works as expected, and only then publish the identical, already-verified artifact to the real PyPI — treating the real publish as the final, confirmed step rather than the first attempt at getting the configuration right.

āœ•
—
+
$ twine upload --repository testpypi dist/*
# Uploads to https://test.pypi.org instead of the real, permanent PyPI

$ pip install --index-url https://test.pypi.org/simple/ my-package
# Verify the ACTUAL published package installs and works correctly
localhost:3000
Safe, Real Practice
TestPyPI: real infrastructure, no permanent consequence
Verify everything works before the real, irreversible publish

3API Tokens, Not Passwords: Scoped, Revocable Authentication

Authenticating a publish with your actual PyPI account password is both a security risk (that password, if leaked from a CI configuration or a local .pypirc file, grants full access to your entire account, every package you maintain) and increasingly disallowed by PyPI's own policies for programmatic access. API tokens are the correct, current authentication method: generated from your PyPI account settings, a token can be scoped to a *specific single project* rather than your whole account, and — critically — can be individually revoked at any time without affecting your actual account password or any other tokens.

Using __token__ as the literal username (a PyPI-specific convention signaling 'this password field actually contains an API token, not an account password') alongside the actual token value is the standard configuration pattern, whether set in a ~/.pypirc file for local publishing or as a CI secret for automated publishing pipelines. The scoping and revocability this provides directly limits the blast radius of a leaked credential — a leaked project-scoped token compromises only that one project's ability to publish new releases, not your entire PyPI account or every other package you maintain.

This follows the exact same principle covered in the Python Security section's treatment of credentials generally: never commit a token or password to version control, use environment variables or a properly-secured secrets mechanism (particularly in CI pipelines), and prefer the most narrowly-scoped, easily-revocable credential available for any given task — publishing to PyPI is a specific, concrete instance of that general discipline.

āœ•
—
+
# ~/.pypirc or environment variable -- an API TOKEN, not a password
[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmc...   # a scoped, revocable token

# NEVER commit this file or token to version control
localhost:3000
Scoped, Revocable Credentials
username = __token__
Project-scoped API token — never the actual account password

4Step-by-Step Breakdown

PyPI does not let you re-upload a version number, ever, even after deleting a bad release. That single fact should change how you approach your first publish.

twine upload sends your BUILT artifacts (from the previous lesson) to PyPI -- it does the actual publishing step, separate from building.

Checkpoint: If version 1.0.0 is published to PyPI with a critical bug, and you delete that release, can you later re-upload a FIXED version also numbered 1.0.0?

  • →No — PyPI permanently reserves a version number once it's been used, even after deletion; the fix must be published as a new, different version number (e.g. 1.0.1)
  • →Yes — deleting a release frees up its version number for reuse

TestPyPI is a SEPARATE, real instance of PyPI for practicing the publish process -- without permanently consuming a real version number.

Checkpoint: What is the main value of publishing to TestPyPI before the real PyPI?

  • →It lets you verify the entire publish-and-install process actually works, without permanently consuming a real version number on a mistake
  • →It uploads packages faster than the real PyPI

API tokens (not your PyPI account password) are the correct authentication method -- scoped, revocable, and never exposing your actual account credentials.

That completes Packaging & Distribution — pyproject.toml, pip, uv, Poetry, building, and now publishing give you the complete path from source code to a real, installable package. Next, Python Security covers the practices that keep that published code safe to depend on.

Check a Real Version Publish Rule. Finish can_publish_version(): PyPI never allows re-uploading a version number.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Always publish to TestPyPI first and verify the package actually installs and works correctly, for any first release or significant configuration change

This catches metadata, build, or package-discovery mistakes before an irreversible action on the real PyPI, given that real PyPI version numbers can never be reused once published.

Use a project-scoped API token, never your actual PyPI account password, for publishing

A scoped, revocable token limits the blast radius of a leaked credential to one project rather than your entire account, and is required by modern PyPI publishing policy.

Frequent Bugs

THE BUG

Publishing directly to the real, permanent PyPI without first verifying via TestPyPI, discovering a configuration mistake (missing files, wrong metadata) only after the version number has already been permanently, irreversibly consumed.

THE FIX

Always publish to TestPyPI first and verify the resulting package installs and works correctly, before publishing the same, already-verified artifact to the real PyPI.

Real-World Examples

A Verified Release Workflow Using TestPyPI Before the Real Publish

A team preparing a library's first public release wants to catch any packaging mistakes before consuming a permanent version number on the real PyPI.

$ python -m build
$ twine upload --repository testpypi dist/*
$ python -m venv /tmp/verify-env && source /tmp/verify-env/bin/activate
$ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ my-package
$ python -c "import my_package; print(my_package.__version__)"  # verify it actually works

# Only after this succeeds:
$ twine upload dist/*  # the real, permanent publish

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Publishing directly to the real PyPI without first verifying via TestPyPI, discovering only afterward that package discovery was misconfigured and the published version is missing a real submodule -- a mistake that now permanently consumes that version number.

# Risky: no verification before the irreversible real publish $ python -m build $ twine upload dist/* # if something's wrong, 1.0.0 is now PERMANENTLY consumed # Correct: verify via TestPyPI first $ python -m build $ twine upload --repository testpypi dist/* $ pip install --index-url https://test.pypi.org/simple/ my-package # verify it works $ twine upload dist/* # only now, the real publish

The Solution //

Always verify a package builds AND installs correctly via TestPyPI first, catching configuration mistakes before they consume a permanent, irreversible version number on the real PyPI.

Lesson Glossary

[01]PyPI

The Python Package Index, the official public registry most Python packages are published to and installed from.

Code Preview
// PyPI context

[02]TestPyPI

A separate, fully-functional PyPI-like instance for practicing the publish process without consuming a real, permanent version number.

Code Preview
// TestPyPI context

[03]twine

A standard tool for securely uploading built package artifacts to PyPI or a compatible index.

Code Preview
// twine context

[04]API token (PyPI)

A scoped, revocable credential (used with username __token__) for authenticating a publish, in place of an actual account password.

Code Preview
// API token (PyPI) context

Continue Learning