Let's Publish a Python Package

2021-10-05 tips-and-tricks Python

PyPi, short for Python Package Index, is the official third-party software repository for Python. PyPi allows users to access, search and filter thousands of Python packages and is some package managers, for instance pip, default source for packages and their depencies.

Registration is possible on PyPi, allowing users to upload their own packages as long as users respects the PyPi Code of Conduct.

Here, we will focus on creating a truly basic Python package and upload it to PyPi.

Repository structure

In this article, we will package a very basic Python calculator. Its structure is explained below.

calculator
|-- README.rst
|-- LICENSE
|-- calculator/
|    |-- __init__.py
|    |-- calculator.py
|    |-- data/
|         |-- __init__.py
|         |-- data.txt
|-- setup.py
|-- tests/

The setup.py file

You must have noticed the setup.py file: it is the build script for setuptools. This file contains all info on the package (name and version for instance) and where to find the code.

from setuptools import setup, find_packages
import os

import calculator

readme_path = os.path.join(os.path.dirname(__file__), "README.rst")

setup(
      name = "mcp_basic_calc_example",
      version = calculator.__version__, 
      url = "<INSERT URL TO PROJECT>",
      author = "<INSERT AUTHOR NAME>",
      author_email = "<INSERT AUTHOR EMAIL>",
      maintainer = "<INSERT MAINTAINER NAME>",
      maintainer_email = "<INSERT MAINTAINER EMAIL>",
      keywords = "<KEYWORD1> <KEYWORD2> <KEYWORD3>",
      classifiers = [
          "Development Status :: 4 - Beta",
          "Programming Language :: Python :: 3.7",
          "License :: OSI Approved :: MIT License",
          "Topic :: Education",
          "Topic :: Documentation"],
      packages = ["calculator", "tests"],
      long_description = open(readme_path).read(),
      long_description_content_type="text/markdown",
      description = "<INSERT DESCRIPTION>",
      python_requires="&gt;=3.7",
      platforms = "ALL",
     )

As you can see, the setup function takes several arguments, including:

  • name, the distribution name of the package. You can pick whatever you want (as long as you only use letters, _ and -), just make sure it’s not already in use on PyPi.
  • version, obviously the package version
  • url of the project homepage
  • author and maintainer information
  • classifiers, package metadata
  • packages, list of all Python import packages (can be replaced by setuptools.find_packages())
  • description and long_description (here, loaded from the README.rst)

Generating distribution archives

First, we need to make sure setuptools and wheel are up-to-date:

python -m pip install --user --upgrade setuptools wheel

All you need to do now is to run the following command from the directory where setup.py is:

python setup.py sdist bdist_wheel

This will output a lot of text. After completion, take a look at the dist/ directory. It should look like this (keep in mind that the name should not be identical since the package name should not be the same since package name must be unique in PyPi and this one is taken):

dist/
|-- mcp_basic_calc_example-0.0.1-py3-none-any.whl
|-- mcp_basic_calc_example-0.0.1.tar.gz

The .whl file is the built distribution and the .tar.gz is the source archive. Nowadays package managers tend to prefer built distributions and fall back to source archives.

Best practices dictate that you upload a source archive and provide a built distribution for all platforms the project is compatible with (yeah! compatibility issues!).

Upload!

PyPi is so wonderful that a separate instance exists for testing purposes: TestPyPi. You can register here. Once that’s done, create an API token and copy the API token before leaving the page.

Then, make sure you have Twine:

python -m pip install --user --upgrade twine

You may run Twine to upload you archives:

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

You should get:

Uploading distributions to https://test.pypi.org/legacy/
Enter your username: __token__
Enter your password: 
Uploading mcp_basic_calc_example-0.0.1-py3-none-any.whl
100%|█████] 8.86k/8.86k [00:01&lt;00:00, 6.16kB/s]
Uploading mcp_basic_calc_example-0.0.1.tar.gz
100%|█████ 6.36k/6.36k [00:00&lt;00:00, 7.97kB/s]

Use __token__ as username and paste your API token as password or set up a Twine configuration file.

Install your package and check everything works

The next step is obviously to try and install the package from TestPyPi and check if everything works fine. Feel free to create a new virtualenv to install your package into.

python -m pip install --index-url https://test.pypi.org/simple/ --no-deps mcp_basic_calc_example

The output should look like that:

Looking in indexes: https://test.pypi.org/simple/
Collecting mcp_basic_calc_example
  Downloading https://test-files.pythonhosted.org/packages/5f/e5/586d97ac505142e88a05e5ed9784ded5b9a969a078e5d14a1b783bd06c9f/mcp_basic_calc_example-0.0.1-py3-none-any.whl (5.1 kB)
Installing collected packages: mcp-basic-calc-example
Successfully installed mcp-basic-calc-example-0.0.1

Yes! Success! Well, now, let’s try to use the package! In the Python interpreter I get:

In [1]: from calculator import calculator
In [2]: mycalc = calculator.BasicCalc()
In [3]: mycalc.secret(2, 7)
Out[3]: 77

Success! Again!

Last step

Everything works? Great! You may upload on PyPi, for real this time (provided you have an account on the real PyPi, an API token, etc):

python3 -m twine upload dist/*

All there is left to do:

  1. Install your own package pip install PKG-NAME
  2. Gloat