Publish Python packages to PyPI (pip)
Published:
A step-by-step pipeline for uploading and publishing a Python package to PyPI that can be installed with pip
. This pipeline includes the repo structure, py project configuration, building and publishing package.
Note that more complex issues may arise if the package is a mixture of multiple languages such as Python and C++.
Pre-requisites
We need Python3, pip
, and an account on PyPI, with 2FA enabled. We also need the following packages, which can be installed with pip
- build
- hatch
- twine
pip install build
pip install hatch
pip install twine
Build python package
Repository dir structure
The structure of my repo dir looks like this. All source code of my package your_pack_name
is in the directory src/your_pack_name
. Other first-level directories (e.g. example
which contains small example test data) won’t be packaged or uploaded .
.
├── LICENSE
├── README.md
├── example
│ └── example.data
├── pyproject.toml
├── requirements.txt
└── src
└── your_pack_name
├── __init__.py
├── main.py
├── source_file1.py
├── source_file2.py
└── util.py
Note that all source files should be organized in the src/your_pack_name
directory, so that after installation, the relevant functions can be imported by import your_pack_name
.
Assuming that main.py
is the entry point of your package when run from the command line (e.g. python main.py --args something
).
__init__.py
An __init__.py
file is necessary for Python to recognize the directory should be treated as a package, not just a regular folder with Python files.
It should contain basic information such as VERSION
, AUTHOR
, and functions that are importable by other scripts.
# __init__.py
VERSION = "0.1.0"
# defines: "from your_pack_name import *"
__all__ = ['func1', 'class2']
# those two are importable
from .source_file1 import func1
from .source_file2 import class2
# import main
from .main import main
We also want to make an executable command for this package, which is essentially the main()
function in main.py
file. This can be configured in the toml
file below.
Configure pyproject.toml
We need a configuration file pyproject.toml
for hatch
to build the package for us. An example looks like this:
# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/your_pack_name"]
[project]
name = "your_pack_name" # name of your package, should not conflict with existing PyPI packages
version = "0.1.0"
license = "BSD-3-Clause" # license of your choice
description = "your_pack_name is a py tool that needs short descriptions to modify here"
authors = [
{ name="Author 1", email="author1@school.edu" },
{ name="Author 2", email="author2@institute.org" },
]
dependencies = [
"numpy>=1.0", # dependencies, it is recommended to include versions of dependencies
"another dependency available in pypi", # all dependencies should already be available in PyPI
"another dependency available in pypi",
]
readme = "README.md" # remember to edit the README
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: BSD License",
]
include = [
"src/your_pack_name/*.py", # only those files will be packed
]
[project.scripts]
# If you want an executable command, e.g. "your_exctable_name --args something"
# It has the effect as "python main.py --args something"
"your_exctable_name" = "your_pack_name.main:main"
[project.urls]
"Homepage" = "https://your-github"
"Bug Tracker" = "https://your-github/issues"
To test whether the toml
file is valid. We can try to install our package to our own computer. Run this command from the same directory as pyproject.toml
.
pip install .
If the package is installed successfully and works as expected, we can continue to build and publish.
Build package
Lastly, build the package using hatch
or build
.
# build with hatch
hatch build
# OR build with build
python -m build
Then the built packages should be available in a new directory dist
, as two files your_pack_name.tar.gz
and your_pack_name.whl
files. Those two files will be eventually uploaded to PyPI.
Optionally, you can unzip your_pack_name.tar.gz
file to ensure it contains all necessary files but no unwanted files.
Publish to PyPI
Optional: test publication on TestPyPI
We can try if everything is right by first publish our package on TestPyPI, so that we won’t accidentally screw-up a production environment. Note that TestPyPI is a different space from PyPI, so a separate TestPyPI account is needed.
Use twine
to publish to testpypi. You will be prompted to enter your credentials.
twine upload --repository testpypi dist/*
Let’s check whether your package can be installed from TestPyPI. Also you should search your package name on TestPyPI to ensure everything is correct.
pip install --index-url https://test.pypi.org/simple/ your_pack_name
Publish to PyPI
We will use twine
to publish the compiled dist
files to PyPI. You will be prompted to enter your credentials.
twine upload --repository pypi dist/*
Let’s check whether your package can be installed from PyPI. Also, you should search your package name on PyPI to ensure everything is correct.
pip install your_pack_name
Great! We just published a package to PyPI! If one is curious, statistics such as the number of downloads can be found on pepy or pypistats.
Optional: PyPI API token
Although it’s OK to use passwords, using an API token is more convenient for frequent package updates/uploads.
An API token can be generated in the Account settings at the bottom of the webpage. Note that all tokens start with pypi
. Since we use twine, it reads the token from ~/.pypirc
. We should edit the file like the following:
# in your $HOME/.pypirc file
[pypi]
username = __token__
password = pypi-your_new_token
From now on, twine
won’t ask for the credentials anymore, but using the token from the ~/.pypirc
file.