# Publishing Your First Python Package to PyPI

This tutorial guides you through publishing a Python package to the Python Package Index ([PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md)) using [uv](https://pydevtools.com/handbook/reference/uv.md). You'll learn the essential steps of building and uploading a package that others can install with [pip](https://pydevtools.com/handbook/reference/pip.md) or [uv](https://pydevtools.com/handbook/reference/uv.md).

Since this is your first package, we'll start by uploading to [TestPyPI](https://test.pypi.org), a "separate instance of the Python Package Index that allows you to try distribution tools and processes without affecting the real index."

## Prerequisites

* [uv](https://pydevtools.com/handbook/reference/uv.md) installed ([installation guide](https://docs.astral.sh/uv/getting-started/installation/))
* A TestPyPI account ([register here](https://test.pypi.org/account/register/))
* 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](https://test.pypi.org). 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.

```console
$ 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:

```console
$ 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
```

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](https://pydevtools.com/handbook/reference/wheel.md) file (`.whl`) - Built distribution
* A [source distribution](https://pydevtools.com/handbook/reference/sdist.md) (`.tar.gz`)

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

1. Log into PyPI
2. Go to [Account Settings](https://test.pypi.org/manage/account/) → [API tokens](https://test.pypi.org/manage/account/#api-tokens)
3. Create a token with "Entire Account" scope
4. Save the token securely - you won't see it again

## Publishing to TestPyPI

Use your saved token to publish:

```console
$ 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)
```

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:

```console
$ 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](https://pypi.org).

Learn More:

* [PyPI Publishing Guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
* [uv Publishing Documentation](https://docs.astral.sh/uv/guides/publish/)
* [Python Package Metadata Specification](https://packaging.python.org/en/latest/specifications/core-metadata/)
* [Publishing package distribution releases using GitHub Actions CI/CD workflows](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/)
