How to Package a Python App (Pip) for PyPi


In this tutorial, we will create a Python application that can be installed directly from pip that will show the 10 latest blog posts from this website (the one you are reading this on!).

If you just want to avoid all of this hard work and publish a python script directly, you can always make use of my makepip package right from the command line.

Learn more about Makepip here.

Getting started

Make sure you have registered at Pypy and have an account, we will need this to upload our package once we’re done.

Now create a directory to work from:

mkdir -p ~/src/tmp/aogl_feed && cd $_

In our new directory, make sure to have a python virtual environment to make life a little simpler.

virtualenv -p python3 venv

And activate it:

source venv/bin/activate

Now make sure that we have all the neccessary things installed to complete this tutorial successfully.

python -m pip install --upgrade pip setuptools wheel
python -m pip install tqdm
python -m pip install twine

Creating our structure and files

At this stage we should create our directory structure for our package:

Because our package will be quite simple to demonstrate what it takes, create the following 4 files:

LICENCE
README.md
aogl/
    __init__.py
    __main__.py
    aogo.py
setup.py

In the licence file, you can place the following (customise it as you need):

Copyright (c) 2020 Andrew O https://andrewodendaal.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

For the readme file, we will add this:

A python package to retrieve the latest 10 blog posts from https://ataiva.com

In the setup.py file, we configure all our project-specific information:

import setuptools

with open("README.md", "r") as fh:
    long_description = fh.read()

setuptools.setup(
    name='aogl',  
    version='0.1',
    author="Andrew Odendaal",
    author_email="[email protected]",
    description="A python package to retrieve the latest 10 blog posts from https://ataiva.com",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/ao/aogl_pip",
    packages=["aogl"],
    entry_points = {
        "console_scripts": ['aogl = aogl.aogl:main']
    },
    install_requires=[
        "requests",
        "feedparser"
    ],
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
)

Create a repository to store everything

You may notice that we have listed the url key to point to https://github.com/ao/aogl_pip, which doesn’t yet exist, so let’s go and create that.

Go to Github and create a new repository.

We will name ours aogl_pip to match with what we told setup.py it would be.

We don’t want to initialise with a README, as we already have one. Click Create repository to proceed.

We can now push to our new repository, but we aren’t quite ready yet. So let’s finish up our local setup.

Add our main code

At this point, we can create our aoms.py file mentioned above and populate it with the following code:

#!/usr/bin/env python

import feedparser, requests
response = requests.get("/feed")
feed = feedparser.parse(response.content)

feed.entries = feed.entries[0:9]

for entry in feed.entries:
  print(entry.title)
  print(entry.links[0].href)
  print()

Also, make sure to set it to be executable:

chmod +x aogl.py

Let us also create the blank aogl/__init__.py file and the aogl/__main__.py file which contains the following code:

from .aogl import main
main()

As we have used a couple dependencies ourselves, will should install them to our virtual environment as follows:

pip install requests
pip install feedparser

Distributing these dependencies is easy when we allow people to install our app through pip, because we have specified these dependencies in the setup.py file, remember this block?

install_requires=[
     "requests",
     "feedparser"
],

Now when we run python aogl.py, our script prints out the 10 latest blog posts for us.

Build our package

It’s now time to build our package and push it to Pypi.

We do this by running:

python setup.py bdist_wheel

This creates a whole lot of files for us, the output looks something like this:

running sdist
running egg_info
creating aogl.egg-info
writing aogl.egg-info/PKG-INFO
writing dependency_links to aogl.egg-info/dependency_links.txt
writing entry points to aogl.egg-info/entry_points.txt
writing top-level names to aogl.egg-info/top_level.txt
writing manifest file 'aogl.egg-info/SOURCES.txt'
reading manifest file 'aogl.egg-info/SOURCES.txt'
writing manifest file 'aogl.egg-info/SOURCES.txt'
running check
creating aogl-0.1
creating aogl-0.1/aogl
creating aogl-0.1/aogl.egg-info
copying files to aogl-0.1...
copying README.md -> aogl-0.1
copying setup.py -> aogl-0.1
copying aogl/__init__.py -> aogl-0.1/aogl
copying aogl/__main__.py -> aogl-0.1/aogl
copying aogl/aogl.py -> aogl-0.1/aogl
copying aogl.egg-info/PKG-INFO -> aogl-0.1/aogl.egg-info
copying aogl.egg-info/SOURCES.txt -> aogl-0.1/aogl.egg-info
copying aogl.egg-info/dependency_links.txt -> aogl-0.1/aogl.egg-info
copying aogl.egg-info/entry_points.txt -> aogl-0.1/aogl.egg-info
copying aogl.egg-info/top_level.txt -> aogl-0.1/aogl.egg-info
Writing aogl-0.1/setup.cfg
creating dist
Creating tar archive
removing 'aogl-0.1' (and everything under it)
running bdist_wheel
running build
running build_py
creating build
creating build/lib
creating build/lib/aogl
copying aogl/__init__.py -> build/lib/aogl
copying aogl/aogl.py -> build/lib/aogl
copying aogl/__main__.py -> build/lib/aogl
installing to build/bdist.macosx-10.15-x86_64/wheel
running install
running install_lib
creating build/bdist.macosx-10.15-x86_64
creating build/bdist.macosx-10.15-x86_64/wheel
creating build/bdist.macosx-10.15-x86_64/wheel/aogl
copying build/lib/aogl/__init__.py -> build/bdist.macosx-10.15-x86_64/wheel/aogl
copying build/lib/aogl/aogl.py -> build/bdist.macosx-10.15-x86_64/wheel/aogl
copying build/lib/aogl/__main__.py -> build/bdist.macosx-10.15-x86_64/wheel/aogl
running install_egg_info
Copying aogl.egg-info to build/bdist.macosx-10.15-x86_64/wheel/aogl-0.1-py3.7.egg-info
running install_scripts
adding license file "LICENCE" (matched pattern "LICEN[CS]E*")
creating build/bdist.macosx-10.15-x86_64/wheel/aogl-0.1.dist-info/WHEEL
creating 'dist/aogl-0.1-py3-none-any.whl' and adding 'build/bdist.macosx-10.15-x86_64/wheel' to it
adding 'aogl/__init__.py'
adding 'aogl/__main__.py'
adding 'aogl/aogl.py'
adding 'aogl-0.1.dist-info/LICENCE'
adding 'aogl-0.1.dist-info/METADATA'
adding 'aogl-0.1.dist-info/WHEEL'
adding 'aogl-0.1.dist-info/entry_points.txt'
adding 'aogl-0.1.dist-info/top_level.txt'
adding 'aogl-0.1.dist-info/RECORD'
removing build/bdist.macosx-10.15-x86_64/wheel

Test our package

Let’s take a look at what was created; using the tree command, we limit the output to 2 levels of depth:

tree -L 2

.
├── LICENCE
├── README.md
├── aogl
│   ├── __init__.py
│   ├── __main__.py
│   └── aogl.py
├── aogl.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── entry_points.txt
│   └── top_level.txt
├── build
│   ├── bdist.macosx-10.15-x86_64
│   └── lib
├── dist
│   ├── aogl-0.1-py3-none-any.whl
│   └── aogl-0.1.tar.gz
├── setup.py
└── venv
    ├── bin
    ├── include
    └── lib

We can see there is a build directory, which contains build package information.

The aogl.egg.info directory contains all dependency and package information.

There is also a dist directory, which contains our *.whl, which is a Wheel file.

This wheel can be installed directory with pip if we wanted to, by running pip install dist/aogl-0.1-py3-none-any.whl.

We will actually do this to make sure that everything works as expected before we publish our new code to the world.

It works pretty well!

Let’s uninstall this local pip, so that we are able to install it from Pypi once it’s successfully pushed.

pip uninstall aogl

Upload our code to Pypi

Next we will create a file under our home directory called ~/.pypirc and configure it:

[distutils] 
index-servers=pypi

[pypi] 
repository = https://upload.pypi.org/legacy/ 
username = aogl

My username happens to be the same as the package I’m currently building, so make sure to adjust the username value to whatever your registered username is on the Pypi website.

Now we can use twine to upload our wheel.

python -m twine upload dist/*

If all was successful, we should see this:

Uploading distributions to https://upload.pypi.org/legacy/
Enter your password:
Uploading aogl-0.1-py3-none-any.whl
100%|█████████████████████████████████████████████| 5.89k/5.89k [00:00<00:00, 7.29kB/s]
NOTE: Try --verbose to see response content.
HTTPError: 403 Client Error: Invalid or non-existent authentication information. See https://pypi.org/help/#invalid-auth for details for url: https://upload.pypi.org/legacy/
(venv) ➜  aogl_feed python -m twine upload dist/*
Uploading distributions to https://upload.pypi.org/legacy/
Enter your password:
Uploading aogl-0.1-py3-none-any.whl
100%|█████████████████████████████████████████████| 5.89k/5.89k [00:03<00:00, 1.96kB/s]
Uploading aogl-0.1.tar.gz
100%|█████████████████████████████████████████████| 4.37k/4.37k [00:01<00:00, 2.93kB/s]

View at:
https:&#47;&#47;pypi.org/project/aogl/0.1/

Our package is now available at https://pypi.org/project/aogl/0.1/

Push our code to Github

Don’t forget to push the code to Github, so that we can update new versions later.

git init
git add LICENCE README.md aogl/ setup.py
git commit -m 'Pushing code for aogl version 0.1'
git remote add origin https://github.com/ao/aogl_pip.git
git push -u origin master

Test everything as the world would see it

Finally we get to test out installing our new package using pip off of Pypi!

pip install aogl

It installed successfully!

aogl

Our wonderful new contribution to the world has returned a list of the latest 10 blog posts!

Excellent! Job done.

Remember, you can also use the <a rel="noreferrer noopener" aria-label="makepip (opens in a new tab)" href="https://pypi.org/project/makepip/" target="_blank">makepip</a> package to do all of this automatically for you!

Learn more about Makepip here .