Publishing Your First Python Package to PyPI
This tutorial guides you through publishing a Python package to the Python Package Index (PyPI) using uv. You’ll learn the essential steps of building and uploading a package that others can install with pip or uv.
Since this is your first package, we’ll start by uploading to TestPyPI, a “separate instance of the Python Package Index that allows you to try distribution tools and processes without affecting the real index.”
Prerequisites
- uv installed (installation guide)
- A TestPyPI account (register here)
- A Python project ready for distribution
Setting Up Your Project
Create a new project with a name that is not currently taken on Test PyPI. You might try something like your name followed by a random number, e.g. timhopper2456543. Browse the project’s URL at https://test.pypi.org/project/<PACKAGE_NAME>/ first; if it returns a project page rather than a 404, the name is taken and uv publish will fail later.
$ uv init <PACKAGE_NAME>
Initialized project `<PACKAGE_NAME>` at `/path/to/<PACKAGE_NAME>`
$ cd <PACKAGE_NAME>
Notice the pyproject.toml uv created. It declares the package name, version, and requires-python. TestPyPI uses those fields to register your release.
Building Distributions
Build your package distributions from the project root:
$ uv build
Building source distribution...
running egg_info
... (setuptools build output) ...
Successfully built dist/<PACKAGE_NAME>-0.1.0.tar.gz
Successfully built dist/<PACKAGE_NAME>-0.1.0-py3-none-any.whl
Between Building source distribution... and the final Successfully built lines, setuptools prints several dozen lines of build progress. The exact lines vary by platform and project structure; what matters is that both Successfully built lines appear at the end.
If you see × Failed to build ...does not appear to be a Python project, as neither pyproject.toml nor setup.py are present, you ran uv build from outside the project. Run cd <PACKAGE_NAME> first.
This creates two files in the dist/ directory:
- A wheel file (
.whl) - Built distribution - A source distribution (
.tar.gz)
Notice the new dist/ directory next to pyproject.toml. Open it and confirm both files are present before continuing to the publish step. If either file is missing, the build stopped partway through; re-run uv build and check the output for an error line.
Notice both filenames carry 0.1.0 from pyproject.toml. TestPyPI rejects re-uploads of an existing version, so any future publish will need a version bump in pyproject.toml followed by another uv build.
Creating a PyPI API Token
- Log into PyPI
- Go to Account Settings → API tokens
- Create a token with “Entire Account” scope
- Save the token securely - you won’t see it again
Publishing to TestPyPI
Use your saved token to publish:
$ uv publish --token pypi-xxxx-xxxx-xxxx-xxxx --publish-url https://test.pypi.org/legacy/
Publishing 2 files to https://test.pypi.org/legacy/
Uploading <PACKAGE_NAME>-0.1.0-py3-none-any.whl (1.3KiB)
Uploading <PACKAGE_NAME>-0.1.0.tar.gz (930B)
Visit https://test.pypi.org/project/<PACKAGE_NAME>/ to confirm the upload landed. A project page with version 0.1.0 should appear within a few seconds. If the page still shows a 404 after a minute, the upload may have failed silently; check the terminal output for a non-zero exit code.
If you see 403 Forbidden, either the token is wrong or another TestPyPI user already owns the project name. Pick a different name, regenerate the project with uv init, and rebuild before retrying. If you see 400 File already exists, you previously uploaded version 0.1.0; bump the version field in pyproject.toml, re-run uv build, and try again.
Verifying Installation
Test that your package installs correctly:
If your package name contains hyphens (for example my-package), the Python import uses underscores instead (my_package). Adjust the -c argument in the command below accordingly.
$ uv run --index https://test.pypi.org/simple --with <PACKAGE_NAME> --no-project -- python -c "import <PACKAGE_NAME>"
The command prints nothing on success. python -c "import ..." exits cleanly when the import works, so silence here means the upload landed and the package installs.
Notice the --no-project flag. It tells uv to ignore the local pyproject.toml so the package is fetched from TestPyPI into a fresh environment instead of resolved from your working directory.
If you see No solution found when resolving dependencies or a 404 on the package URL, TestPyPI may not have indexed the upload yet. Wait a minute and try again.
Publish to PyPI
When you’re ready to publish to PyPI, you will remove the --publish_url from the publish command and use a token created at pypi.org.
Learn More:
- PyPI Publishing Guide
- uv Publishing Documentation
- Python Package Metadata Specification
- Publishing package distribution releases using GitHub Actions CI/CD workflows
- pyOpenSci Python Package Guide — covers documentation, licensing, contributor guides, and scientific Python packaging conventions