How to Publish a Python Package to PyPI

Do you have a Python package that you’d like to share with the world? You should publish it on the Python Package Index (PyPI). The vast majority of Python packages are published there. The PyPI team has also created extensive documentation to help you on your packaging journey. This article does not aim to replace that documentation. Instead, it is just a shorter version of it using ObjectListView as an example.

The ObjectListView project for Python is based on a C# wrapper .NET ListView but for wxPython. You use ObjectListView as a replacement for wx.ListCtrl because its methods and attributes are simpler. Unfortunately, the original implementation died out in 2015 while a fork, ObjectListView2 died in 2019. For this article, you will learn how I forked it again and created ObjectListView3 and packaged it up for PyPI.

Creating a Package Structure

When you create a Python package, you must follow a certain type of directory structure to build everything correctly. For example, your package files should go inside a folder named src. Within that folder, you will have your package sub-folder that contains something like an __init__.py and your other Python files.

The src folder’s contents will look something like this:

package_tutorial/
| -- src/
|    -- your_amazing_package/
|        -- __init__.py
|        -- example.py

The __init__.py file can be empty. You use that file to tell Python that the folder is a package and is importable. But wait! There’s more to creating a package than just your Python files!

You also should include the following:

  • A license file
  • The pyproject.toml file which is used for configuring your package when you build it, among other things
  • A README.md file to describe the package, how to install it, example usage, etc
  • A tests folder, if you have any (and you should!)

Go ahead and create these files and the tests folder. It’s okay if they are all blank right now. At this point, your folder structure will look like this:

package_tutorial/
| -- LICENSE
| -- pyproject.toml
| -- README.md
| -- src/
|    -- your_amazing_package/
|        -- __init__.py
|        -- example.py
| -- tests/

Picking a Build Backend

The packaging tutorial mentions that you can choose various build backends for when you create your package. There are examples for the following:

  • Hatchling
  • setuptools
  • Flit
  • PDM

You add this build information in your pyproject.toml file. Here is an example you might use if you picked setuptools:

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

This section in your config is used by pip or build. These tools don’t actually do the heavy lifting of converting your source code into a wheel or other distribution package. That is handled by the build backend. You don’t have to add this section to your pyproject.toml file though. You will find that pip will default to setuptools if there isn’t anything listed.

Configuring Metadata

All the metadata for your package should go into a pyproject.toml file. In the case of ObjectListView, it didn’t have one of these files at all.

Here’s the generic example that the Packaging documentation gives:

[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
authors = [
  { name="Example Author", email="author@example.com" },
]
description = "A small example package"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

[project.urls]
Homepage = "https://github.com/pypa/sampleproject"
Issues = "https://github.com/pypa/sampleproject/issues"

Using this as a template, I created the following for the ObjectListView package:

[project]
name = "ObjectListView3"
version = "1.3.4"
authors = [
  { name="Mike Driscoll", email="mike@somewhere.org" },
]
description = "An ObjectListView is a wrapper around the wx.ListCtrl that makes the list control easier to use."
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

[project.urls]
Homepage = "https://github.com/driscollis/ObjectListView3"
Issues = "https://github.com/driscollis/ObjectListView3/issues"

Now let’s go over what the various parts are in the above metadata:

  • name – The distribution name of your package. Make sure your name is unique!
  • version – The package version
  • authors – One or more authors including emails for each
  • description – A short, one-sentence summary of your package
  • readme – A path to the file that contains a detailed description of the package. You may use a Markdown file here.
  • requires-python – Tells which Python versions are supported by this package
  • classifiers – Gives an index and some metadata for pip
  • URLs – Extra links that you want to show on PyPI. In general, you would want to link to the source, documentation, issue trackers, etc.

You can specify other information in your TOML file if you’d like. For full details, see the pyproject.toml guide.

READMEs and Licenses

The README file is almost always a Markdown file now. Take a look at other popular Python packages to see what you should include. Here are some recommended items:

  • How to install the package
  • Basic usage examples
  • Link to a guide or tutorial
  • An FAQ if you have one

There are many licenses out there. Don’t take advice from anyone; buy a lawyer on that unless you know a lot about this topic. However, you can look at the licenses for other popular packages and use their license or one similar.

Generating a Package

Now you have the files you need, you are ready to generate your package. You will need to make sure you have PyPA’s build installed.

Here’s the command you’ll need for that:

python3 -m pip install --upgrade build

Next, you’ll want to run the following command from the same directory that your pyproject.toml file is in:

python3 -m build

You’ll see a bunch of output from that command. When it’s done, you will have a dist A folder with a wheel (*.whl) file and a gzipped tarball inside it is called a built distribution. The tarball is a source distribution , while the wheel file is called a built distribution. When you use pip, it will try to find the built distribution first, but pip will fall back to the tarball if necessary.

Uploading / Publishing to PyPI

You now have the files you need to publish to share your package with the world on the Python Package Index (PyPI). However, you need to register an account on TestPyPI first. TestPyPI  is a separate package index intended for testing and experimentation, which is perfect when you have never published a package before. To register an account, go to https://test.pypi.org/account/register/ and complete the steps on that page. It will require you to verify your email address, but other than that, it’s a very straightforward signup.

To securely upload your project, you’ll need a PyPI API token. Create one from your account and make sure to set the “Scope” to the “Entire account”. Don’t close the page until you have copied and saved your token or you’ll need to generate another one!

The last step before publishing is to install twine, a tool for uploading packages to PyPI. Here’s the command you’ll need to run in your terminal to get twine:

python3 -m pip install --upgrade twine

Once that has finished installing, you can run twine to upload your files. Make sure you run this command in your package folder where the new dist folder is:

python3 -m twine upload --repository testpypi dist/*

You will see a prompt for your TestPyPI username and/or a password. The password is the API token you saved earlier. The directions in the documentation state that you should use __token__ as your username, but I don’t think it even asked for a username when I ran this command. I believe it only needed the API token itself.

After the command is complete, you will see some text stating which files were uploaded. You can view your package at https://test.pypi.org/project/example_package_name

To verify you can install your new package, run this command:

python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps example-package-name

If you specify the correct name, the package should be downloaded and installed. TestPyPI is not meant for permanent storage, though, so it will likely delete your package eventually to conserve space.

When you have thoroughly tested your package, you’ll want to upload it to the real PyPI site. Here are the steps you’ll need to follow:

  • Pick a memorable and unique package name
  • Register an account at https://pypi.org. You’ll go through the same steps as you did on TestPyPI
    • Be sure to verify your email
    • Create an API key and save it on your machine or in a password vault
  • Use twine to upload the dist folder like this:  python -m twine upload dist/*
  • Install the package from the real PyPI as you normally would

That’s it! You’ve just published your first package!

Wrapping Up

Creating a Python package takes time and thought. You want your package to be easy to install and easy to understand. Be sure to spend enough time on your README and other documentation to make using your package easy and fun. It’s not good to look up a package and not know which versions of Python it supports or how to install it. Make the process easy by uploading your package to the Python Package Index.