Your First Pip Package In Python

Learn how to deploy your first package to Pip in this straight-forward tutorial.

On this page

This is Day 21 of the #100DaysOfPython challenge.

This post will use the setuptools package to deploy a package to the Pip package repository.

It will work off the code written in my post "PyTest With GitHub Actions".

Final project code can be found here.

Prerequisites

  1. Familiarity with Pipenv. See here for my post on Pipenv.
  2. You must have a registered PyPi account.

Getting started

Let's clone the start repository and add the files and packages we will need to deploy.

# Make the `hello-world` directory
$ git clone https://github.com/okeeffed/hello-pytest-github-actions your-first-pip-package-in-python
$ cd your-first-pip-package-in-python
# Install dependencies
$ pipenv install

# Install the setuptools package
$ pipenv install --dev setuptools wheel twine tqdm

# Add some files to prepare for our package shipping
$ touch setup.py LICENSE MANIFEST.in

At this stage, the repo will now be ready to add our required code to deploy to Pip.

Exploring the current repo

I will use tree -L 2 -a to display the current repo:

.
├── .git
│   ├── COMMIT_EDITMSG
│   ├── FETCH_HEAD
│   ├── HEAD
│   ├── config
│   ├── description
│   ├── hooks
│   ├── index
│   ├── info
│   ├── logs
│   ├── objects
│   └── refs
├── .github
│   └── workflows
├── .gitignore
├── .mypy_cache
│   ├── .gitignore
│   ├── 3.9
│   └── CACHEDIR.TAG
├── .pytest_cache
│   ├── .gitignore
│   ├── CACHEDIR.TAG
│   ├── README.md
│   └── v
├── .venv
│   ├── .gitignore
│   ├── .project
│   ├── bin
│   ├── lib
│   ├── pyvenv.cfg
│   └── src
├── Pipfile
├── Pipfile.lock
├── README.md
├── src
│   ├── __init__.py
│   └── math.py
└── tests
    ├── __init__.py
    └── test_math.py

What is important for us here to know is that src/math.py contains the code which we will want to deploy and tests/test_math.py contains a simple test for our code.

If we look at the test file, we can see the code we want to deploy contains simple math functions:

from src.math import add, subtract, multiply


def test_add():
    assert add(3, 2) == 5


def test_subtract():
    assert subtract(3, 2) == 1


def test_multiply():
    assert multiply(3, 2) == 6

We can confirm that this works as expected by running pipenv run pytest.

$ pipenv run pytest
===================== test session starts =====================
platform darwin -- Python 3.9.6, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /path/to/your-first-pip-package-in-python
collected 3 items

tests/test_math.py ...                                  [100%]

====================== 3 passed in 0.03s ======================

We can now focus on deploying this as a package.

Altering our files for a package

We need to move the src folder to be the name of the package we want demo-pip-math:

mv src demo-pip-math

We can now adjust our tests/test_math.py file to reflect this:

from demo_pip_math.math import add, subtract, multiply


def test_add():
    assert add(3, 2) == 5


def test_subtract():
    assert subtract(3, 2) == 1


def test_multiply():
    assert multiply(3, 2) == 6

You can run pipenv run pytest to confirm everything is as it should be.

Editing our setup.py file

We need to edit setup.py to use the setuptools package and contain information about our package:

import setuptools

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

setuptools.setup(
    name="demo_pip_math",
    version="0.1.0",
    author="Dennis O'Keeffe",
    author_email="hello@dennisokeeffe.com",
    description="Demo your first Pip package.",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/okeeffed/your-first-pip-package-in-python",
    packages=setuptools.find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.6',
    keywords='pip-demo math',
    project_urls={
        'Homepage': 'https://github.com/okeeffed/your-first-pip-package-in-python',
    },

)

Our setup.py makes use of the setuptools.setup function to prepare our package.

Some of the more important arguments we are passing:

Argument Does
name Name of the package
version Current package version in semantic versioning
author Your name
description Your package description
long_description A description that comes from our README.md file
python_requires What versions of Python does your package work with
keywords Words to match the package when searching for it

Add a license

You can choose a license to copy into our LICENSE file. I have gone with the MIT license:

Copyright (c) 2021 Dennis O'Keeffe

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.

Adding a README

I am adding a simple README for the sake of completing the package deployment. My README.md file reads as follows:

# Your first Pip package in Python

This repository is the companion post to my blog ["Your first Pip package in Python"](https://blog.dennisokeeffe.com)

## Usage

```py
from demo_pip_math.math import add, subtract, multiply


def test_add():
    assert add(3, 2) == 5


def test_subtract():
    assert subtract(3, 2) == 1


def test_multiply():
    assert multiply(3, 2) == 6
```

Include non-Python files in upload

To enure our README.md and LICENSE are included, we can edit our MANIFEST.in file to have the following:

include README.md
include LICENSE

Packaging the files

Let's edit our Pipfile and add the following to [scripts]:

[scripts]
test = "pytest"
build = "python3 setup.py sdist bdist_wheel" # Added line here
deploy = "twine upload dist/*" # Added line here

This will enable us to run pipenv run build to generate our distribution archives and pipenv run deploy to upload our dist/ folder.

$ pipenv run build
running sdist
running egg_info
creating demo_pip_math.egg-info
writing demo_pip_math.egg-info/PKG-INFO
writing dependency_links to demo_pip_math.egg-info/dependency_links.txt
writing top-level names to demo_pip_math.egg-info/top_level.txt
writing manifest file 'demo_pip_math.egg-info/SOURCES.txt'
reading manifest file 'demo_pip_math.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'demo_pip_math.egg-info/SOURCES.txt'
running check
creating demo_pip_math-0.1.0
creating demo_pip_math-0.1.0/demo_pip_math
creating demo_pip_math-0.1.0/demo_pip_math.egg-info
creating demo_pip_math-0.1.0/tests
copying files to demo_pip_math-0.1.0...
copying LICENSE -> demo_pip_math-0.1.0
copying MANIFEST.in -> demo_pip_math-0.1.0
copying README.md -> demo_pip_math-0.1.0
copying setup.py -> demo_pip_math-0.1.0
copying demo_pip_math/__init__.py -> demo_pip_math-0.1.0/demo_pip_math
copying demo_pip_math/math.py -> demo_pip_math-0.1.0/demo_pip_math
copying demo_pip_math.egg-info/PKG-INFO -> demo_pip_math-0.1.0/demo_pip_math.egg-info
copying demo_pip_math.egg-info/SOURCES.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info
copying demo_pip_math.egg-info/dependency_links.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info
copying demo_pip_math.egg-info/top_level.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info
copying tests/__init__.py -> demo_pip_math-0.1.0/tests
copying tests/test_math.py -> demo_pip_math-0.1.0/tests
Writing demo_pip_math-0.1.0/setup.cfg
creating dist
Creating tar archive
removing 'demo_pip_math-0.1.0' (and everything under it)
running bdist_wheel
running build
running build_py
creating build
creating build/lib
creating build/lib/demo_pip_math
copying demo_pip_math/__init__.py -> build/lib/demo_pip_math
copying demo_pip_math/math.py -> build/lib/demo_pip_math
creating build/lib/tests
copying tests/__init__.py -> build/lib/tests
copying tests/test_math.py -> build/lib/tests
warning: build_py: byte-compiling is disabled, skipping.

installing to build/bdist.macosx-11-x86_64/wheel
running install
running install_lib
creating build/bdist.macosx-11-x86_64
creating build/bdist.macosx-11-x86_64/wheel
creating build/bdist.macosx-11-x86_64/wheel/demo_pip_math
copying build/lib/demo_pip_math/__init__.py -> build/bdist.macosx-11-x86_64/wheel/demo_pip_math
copying build/lib/demo_pip_math/math.py -> build/bdist.macosx-11-x86_64/wheel/demo_pip_math
creating build/bdist.macosx-11-x86_64/wheel/tests
copying build/lib/tests/__init__.py -> build/bdist.macosx-11-x86_64/wheel/tests
copying build/lib/tests/test_math.py -> build/bdist.macosx-11-x86_64/wheel/tests
warning: install_lib: byte-compiling is disabled, skipping.

running install_egg_info
Copying demo_pip_math.egg-info to build/bdist.macosx-11-x86_64/wheel/demo_pip_math-0.1.0-py3.9.egg-info
running install_scripts
adding license file "LICENSE" (matched pattern "LICEN[CS]E*")
creating build/bdist.macosx-11-x86_64/wheel/demo_pip_math-0.1.0.dist-info/WHEEL
creating 'dist/demo_pip_math-0.1.0-py3-none-any.whl' and adding 'build/bdist.macosx-11-x86_64/wheel' to it
adding 'demo_pip_math/__init__.py'
adding 'demo_pip_math/math.py'
adding 'tests/__init__.py'
adding 'tests/test_math.py'
adding 'demo_pip_math-0.1.0.dist-info/LICENSE'
adding 'demo_pip_math-0.1.0.dist-info/METADATA'
adding 'demo_pip_math-0.1.0.dist-info/WHEEL'
adding 'demo_pip_math-0.1.0.dist-info/top_level.txt'
adding 'demo_pip_math-0.1.0.dist-info/RECORD'
removing build/bdist.macosx-11-x86_64/wheel

$ pipenv run deploy
Uploading distributions to https://upload.pypi.org/legacy/
Enter your username: dennisokeeffe
Enter your password:
Uploading demo_pip_math-0.1.0-py3-none-any.whl
100%|█████████████████████| 6.80k/6.80k [00:02<00:00, 2.90kB/s]
Uploading demo_pip_math-0.1.0.tar.gz
100%|█████████████████████| 6.12k/6.12k [00:01<00:00, 3.83kB/s]

View at:
https://pypi.org/project/demo-pip-math/0.1.0/

In my particular case, if I now head to the provided project link then I can see my package has now been deployed!

Package on PyPi
Package on PyPi

Testing out the package

To test out the new package, you can create a new project and install:

$ mkdir test-pip-package
$ cd test-pip-package
$ pipenv --three
$ pipenv install demo-pip-math==0.1.0
$ pipenv shell
$ touch main.py

Add the following code to main.py:

from demo_pip_math.math import add, subtract, multiply


print(add(3, 2))  # print 5
print(subtract(3, 2))  # print 1
print(multiply(3, 2))  # print 6

Run python main.py:

$ python main.py
5
1
6

Great success!

Summary

Today's post demonstrated how to use the Pillow package to add text and icons layers to an image programmatically.

This will be the technique I use for helping to build out my blog post main images (including this one!).

Resources and further reading

  1. The ABCs of Pipenv
  2. Pipenv
  3. Choose a license
  4. MIT license
  5. Test repo for installing new package
  6. "PyTest With GitHub Actions"
  7. Final project code
  8. Register PyPi account
  9. Start repository

Photo credit: teodosiev