1. Code
  2. Python

How to Write Your Own Python Packages

Scroll to top

Python is a wonderful programming language and much more. One of its weakest points, however, is packaging. This is a well-known fact in the community. Installing, importing, using, and creating packages has improved over the years, but it's still not on par with newer languages like Go and Rust that could learn a lot from the struggles of Python and other more mature languages.

In this tutorial, you'll learn everything you need to know to build and share your own packages. For general background on Python packages, please read How to Use Python Packages.

Packaging a Project

Packaging a project is the process by which you take a hopefully coherent set of Python modules and possibly other files and put them in a structure that can be used easily. There are various things you have to consider, such as dependencies on other packages, internal structure (sub-packages), versioning, target audience, and the form of the package (source and/or binary).

Example

Let's start with a quick example. The conman package is a package for managing configuration. It supports several file formats as well as distributed configuration using etcd.

A package's contents are typically stored in a single directory (although it is common to split sub-packages in multiple directories) and sometimes, as in this case, in its own git repository.

The root directory contains various configuration files (setup.py is mandatory and the most important one), and the package code itself is usually in a subdirectory whose name is the name of the package and ideally a tests directory. Here is what it looks like for conman:

1
> tree
2
.
3
├── LICENSE
4
├── MANIFEST.in
5
├── README.md
6
├── conman
7
│   ├── __init__.py
8
│   ├── __pycache__
9
│   ├── conman_base.py
10
│   ├── conman_etcd.py
11
│   └── conman_file.py
12
├── requirements.txt
13
├── setup.cfg
14
├── setup.py
15
├── test-requirements.txt
16
├── tests
17
│   ├── __pycache__
18
│   ├── conman_etcd_test.py
19
│   ├── conman_file_test.py
20
│   └── etcd_test_util.py
21
└── tox.ini
 

Let's take a quick peek at the setup.py file. It imports two functions from the setuptools package: setup() and find_packages(). Then it calls the setup() function and uses find_packages() for one of the parameters.

1
from setuptools import setup, find_packages
2
setup(name='conman',
3
      version='0.3',
4
      url='https://github.com/the-gigi/conman',
5
      license='MIT',
6
      author='Gigi Sayfan',
7
      author_email='the.gigi@gmail.com',
8
      description='Manage configuration files',
9
      packages=find_packages(exclude=['tests']),
10
      long_description=open('README.md').read(),
11
      zip_safe=False,
12
      setup_requires=['nose>=1.0'],
13
      test_suite='nose.collector')
 

This is pretty normal. While the setup.py file is a regular Python file and you can do whatever you want in it, its primary job is to call the setup() function with the appropriate parameters because it will be invoked by various tools in a standard way when installing your package. I'll go over the details in the next section.

The Configuration Files

In addition to setup.py, there are a few other optional configuration files that can show up here and serve various purposes.

setup.py

The setup() function takes a large number of named arguments to control many aspects of package installation as well as running various commands. Many arguments specify metadata used for searching and filtering when uploading your package to a repository.

  • name: the name of your package (and how it will be listed on PyPI)
  • version: this is critical for maintaining proper dependency management
  • url: the URL of your package, typically GitHub or maybe the readthedocs URL
  • packages: list of sub-packages that need to be included; find_packages() helps here
  • setup_requires: here you specify dependencies
  • test_suite: which tool to run at test time

The long_description is set here to the contents of the README.md file, which is a best practice to have a single source of truth.

setup.cfg

The setup.py file also serves a command-line interface to run various commands. For example, to run the unit tests, you can type: python setup.py test

 
1
running test 

2


3
running egg_info
4
writing conman.egg-info/PKG-INFO
5
writing top-level names to conman.egg-info/top_level.txt
6
writing dependency_links to conman.egg-info/dependency_links.txt
7
reading manifest file 'conman.egg-info/SOURCES.txt'
8
reading manifest template 'MANIFEST.in'
9
writing manifest file 'conman.egg-info/SOURCES.txt'
10
running build_ext
11
test_add_bad_key (conman_etcd_test.ConManEtcdTest) ... ok
12
test_add_good_key (conman_etcd_test.ConManEtcdTest) ... ok
13
test_dictionary_access (conman_etcd_test.ConManEtcdTest) ... ok
14
test_initialization (conman_etcd_test.ConManEtcdTest) ... ok
15
test_refresh (conman_etcd_test.ConManEtcdTest) ... ok
16
test_add_config_file_from_env_var (conman_file_test.ConmanFileTest) ... ok
17
test_add_config_file_simple_guess_file_type (conman_file_test.ConmanFileTest) ... ok
18
test_add_config_file_simple_unknown_wrong_file_type (conman_file_test.ConmanFileTest) ... ok
19
test_add_config_file_simple_with_file_type (conman_file_test.ConmanFileTest) ... ok
20
test_add_config_file_simple_wrong_file_type (conman_file_test.ConmanFileTest) ... ok
21
test_add_config_file_with_base_dir (conman_file_test.ConmanFileTest) ... ok
22
test_dictionary_access (conman_file_test.ConmanFileTest) ... ok
23
test_guess_file_type (conman_file_test.ConmanFileTest) ... ok
24
test_init_no_files (conman_file_test.ConmanFileTest) ... ok
25
test_init_some_bad_files (conman_file_test.ConmanFileTest) ... ok
26
test_init_some_good_files (conman_file_test.ConmanFileTest) ... ok
27
----------------------------------------------------------------------
28
Ran 16 tests in 0.160s
29
OK

The setup.cfg is an ini format file that may contain option defaults for commands you pass to setup.py. Here, setup.cfg contains some options for nosetests (our test runner):

1
[nosetests]
2
verbose=1
3
nocapture=1
 

MANIFEST.in

This file contains files that are not part of the internal package directory, but you still want to include. Those are typically the readme file, the license file, etc. An important file is requirements.txt. This file is used by pip to install other required packages.

Here is conman's MANIFEST.in file:

1
include LICENSE
2
include README.md
3
include requirements.txt
 

Dependencies

You can specify dependencies both in the install_requires section of setup.py and in a requirements.txt file. Pip will install automatically dependencies from install_requires, but not from the requirements.txt file. To install those requirements, you'll have to specify it explicitly when running pip: pip install -r requirements.txt.

The install_requires option is designed to specify minimal and more abstract requirements at the major version level. The requirements.txt file is for more concrete requirements, often with pinned-down minor versions.

Here is the requirements file of conman. You can see that all the versions are pinned, which means it can be negatively impacted if one of these packages upgrades and introduces a change that breaks conman.

1
PyYAML==3.11
2
python-etcd==0.4.3
3
urllib3==1.7
4
pyOpenSSL==0.15.1
5
psutil==4.0.0
6
six==1.7.3
 

Pinning gives you predictability and peace of mind. This is especially important if many people install your package at different times. Without pinning, each person will get a different mix of dependency versions based on when they installed it. The downside of pinning is that if you don't keep up with your dependencies development, you may get stuck on an old, poorly performing and even vulnerable version of some dependency.

I originally wrote conman in 2014 and didn't pay much attention to it. Now, for this tutorial I upgraded everything and there were some major improvements across the board for almost every dependency.

Distributions

You can create a source distribution or a binary distribution. I'll cover both.

Source Distribution

You create a source distribution with the command: python setup.py sdist. Here is the output for conman:

1
> python setup.py sdist
2
running sdist
3
running egg_info
4
writing conman.egg-info/PKG-INFO
5
writing top-level names to conman.egg-info/top_level.txt
6
writing dependency_links to conman.egg-info/dependency_links.txt
7
reading manifest file 'conman.egg-info/SOURCES.txt'
8
reading manifest template 'MANIFEST.in'
9
writing manifest file 'conman.egg-info/SOURCES.txt'
10
warning: sdist: standard file not found: should have one of README, README.rst, README.txt
11
running check
12
creating conman-0.3
13
creating conman-0.3/conman
14
creating conman-0.3/conman.egg-info
15
making hard links in conman-0.3...
16
hard linking LICENSE -> conman-0.3
17
hard linking MANIFEST.in -> conman-0.3
18
hard linking README.md -> conman-0.3
19
hard linking requirements.txt -> conman-0.3
20
hard linking setup.cfg -> conman-0.3
21
hard linking setup.py -> conman-0.3
22
hard linking conman/__init__.py -> conman-0.3/conman
23
hard linking conman/conman_base.py -> conman-0.3/conman
24
hard linking conman/conman_etcd.py -> conman-0.3/conman
25
hard linking conman/conman_file.py -> conman-0.3/conman
26
hard linking conman.egg-info/PKG-INFO -> conman-0.3/conman.egg-info
27
hard linking conman.egg-info/SOURCES.txt -> conman-0.3/conman.egg-info
28
hard linking conman.egg-info/dependency_links.txt -> conman-0.3/conman.egg-info
29
hard linking conman.egg-info/not-zip-safe -> conman-0.3/conman.egg-info
30
hard linking conman.egg-info/top_level.txt -> conman-0.3/conman.egg-info
31
copying setup.cfg -> conman-0.3
32
Writing conman-0.3/setup.cfg
33
creating dist
34
Creating tar archive
35
removing 'conman-0.3' (and everything under it)
 

As you can see, I got one warning about missing a README file with one of the standard prefixes because I like Markdown so I have a README.md instead. Other than that, all the package source files were included and the additional files. Then, a bunch of metadata was created in the conman.egg-info directory. Finally, a compressed tar archive called conman-0.3.tar.gz is created and put into a dist sub-directory.

Installing this package will require a build step (even though it's pure Python). You can install it using pip normally, just by passing the path to the package. For example:

1
pip install dist/conman-0.3.tar.gz
2
Processing ./dist/conman-0.3.tar.gz
3
Installing collected packages: conman
4
  Running setup.py install for conman ... done 

5


6
Successfully installed conman-0.3
 

Conman has been installed into site-packages and can be imported like any other package:

1
import conman
2
conman.__file__
3
'/Users/gigi/.virtualenvs/conman/lib/python2.7/site-packages/conman/__init__.pyc'
 

Wheels

Wheels are a relatively new way to package Python code and optionally C extensions. They replace the egg format. There are several types of wheels: pure Python wheels, platform wheels, and universal wheels. The pure Python wheels are packages like conman that don't have any C extension code.

The platform wheels do have C extension code. The universal wheels are pure Python wheels that are compatible with both Python 2 and Python 3 with the same code base (they don't require even 2 to 3).

If you have a pure Python package and you want your package to support both Python 2 and Python 3 (becoming more and more important) then you can build a single universal build instead of one wheel for Python 2 and one wheel for Python 3.

Python 3 is the current and actively supported Python version with ongoing updates, improvements, and community support; it is recommended to use Python 3 for all new projects and migrations.

If your package has C extension code, you must build a platform wheel for each platform. The huge benefit of wheels especially for packages with C extensions is that there is no need to have compiler and supporting libraries available on the target machine. The wheel already contains a built package. So you know it will not fail to build, and it is much faster to install because it is literally just a copy. People who use scientific libraries like Numpy and Pandas can really appreciate this, as installing such packages used to take a long time and might have failed if some library was missing or the compiler wasn't configured properly.

The command to build pure or platform wheels is: python setup.py bdist_wheel.

Setuptools—the engine that provides the setup() function—will detect automatically if a pure or platform wheel is needed.

1
running bdist_wheel
2
running build
3
running build_py
4
creating build
5
creating build/lib
6
creating build/lib/conman
7
copying conman/__init__.py -> build/lib/conman
8
copying conman/conman_base.py -> build/lib/conman
9
copying conman/conman_etcd.py -> build/lib/conman
10
copying conman/conman_file.py -> build/lib/conman
11
installing to build/bdist.macosx-10.9-x86_64/wheel
12
running install 

13


14
running install_lib
15
creating build/bdist.macosx-10.9-x86_64
16
creating build/bdist.macosx-10.9-x86_64/wheel
17
creating build/bdist.macosx-10.9-x86_64/wheel/conman
18
copying build/lib/conman/__init__.py -> build/bdist.macosx-10.9-x86_64/wheel/conman
19
copying build/lib/conman/conman_base.py -> build/bdist.macosx-10.9-x86_64/wheel/conman
20
copying build/lib/conman/conman_etcd.py -> build/bdist.macosx-10.9-x86_64/wheel/conman
21
copying build/lib/conman/conman_file.py -> build/bdist.macosx-10.9-x86_64/wheel/conman
22
running install_egg_info
23
running egg_info
24
creating conman.egg-info
25
writing conman.egg-info/PKG-INFO
26
writing top-level names to conman.egg-info/top_level.txt
27
writing dependency_links to conman.egg-info/dependency_links.txt
28
writing manifest file 'conman.egg-info/SOURCES.txt'
29
reading manifest file 'conman.egg-info/SOURCES.txt'
30
reading manifest template 'MANIFEST.in'
31
writing manifest file 'conman.egg-info/SOURCES.txt'
32
Copying conman.egg-info to build/bdist.macosx-10.9-x86_64/wheel/conman-0.3-py2.7.egg-info
33
running install_scripts
34
creating build/bdist.macosx-10.9-x86_64/wheel/conman-0.3.dist-info/WHEEL
 

Checking the dist directory, you can see that a pure Python wheel was created:

1
ls -la dist
2
dist/
3
total 32
4
-rw-r--r--  1 gigi  staff   5.5K Feb 29 07:57 conman-0.3-py2-none-any.whl
5
-rw-r--r--  1 gigi  staff   4.4K Feb 28 23:33 conman-0.3.tar.gz
 

The name conman-0.3-py2-none-any.whl has several components:

  • package name
  • package version
  • Python version
  • platform version, and finally the "whl" extension.

To build universal packages, you just add --universal, as in python setup.py bdist_wheel --universal.

The resulting wheel is called conman-0.3-py2.py3-none-any.whl.

Note that it is your responsibility to ensure your code actually works under both Python 2 and Python 3 if you create a universal package.

Using Toml

Toml (Tom's Obvious Minimal Language) is an easy-to-use configuration file format for configuring software applications. Even though setup.py is still widely used and supported, PEP518 recommends using a pyproject.toml file instead of setup.py for packaging and distribution. 

Let's use pyproject.toml to create a simple Python package called apex that checks the battery status and displays a message when the battery is full.

Create a directory structure called apex. Inside apex, add a pyproject.toml and a README.md file.

1
.apex
2
├── pyproject.toml
3
└── README.md

Inside the root apex directory, create a subdirectory called apex with an __init__.py file to make it a package and a battery.py file. Your project directory should now look like this:

1
.
2
├── apex
3
│   ├── battery.py
4
│   └── __init__.py
5
├── pyproject.toml
6
└── README.md

Install the psutil package with pip.

1
pip install psutil

Psutil (processes and system utilities) is a cross-platform library for process and system monitoring in Python. Psutil can retrieve information about processes running on the system, such as status, CPU usage, and memory usage. It can also manipulate system processes. 

In the battery.py file, add the code that checks if the battery is full.

1
import psutil
2
3
def check_battery():
4
    battery = psutil.sensors_battery()
5
    plugged = battery.power_plugged
6
    percent = battery.percent
7
8
    if percent == 100 and plugged:
9
        print "Battery is full. Unplug your Charger"
10
    else:
11
        print "Battery is not yet full."
12
13
if __name__ == "__main__":
14
    message = check_battery()
15
    print(message)

The pyproject.toml file contains the metadata for the package, including:

  • build-system
  • name
  • version
  • project-dependencies.

Open pyproject.toml and specify setuptools as the build system.

1
[build-system]
2
requires = ["setuptools","wheel"]
3
build-backend ="setuptools.build_meta"

Specify the name of the packages, the version number, and the list of project dependencies. Now the pyproject.toml file looks like this:

1
[build-system]
2
requires = ["setuptools","wheel"]
3
build-backend ="setuptools.build_meta"
4
5
[project]
6
name="apex"
7
version ="1.0.0"
8
9
[project-dependencies]
10
psutil ="5.9.5"

Finally, in the README.md file, add the following contents.

1
# apex
2
3
**apex** is a Python package that allows you to check the battery status of your laptop and display a message when the battery is full.
4
5
## Features
6
7
- Retrieve battery status information
8
- Display a message when the battery is full
9
- Cross-platform support (Windows, macOS, Linux, etc.)
10
11
## Installation
12
13
Install **apex** using `pip`:
14
15
```shell
16
pip install apex

Build the package with the build command.

1
 python -m build

You should see the output below, which I have truncated. 

1
creating build/bdist.linux-x86_64/wheel/apex-1.0.0.dist-info/WHEEL
2
creating '/home/vaati/Desktop/apex/dist/.tmp-d8_lqaaj/apex-1.0.0-py3-none-any.whl' and adding 'build/bdist.linux-x86_64/wheel' to it
3
adding 'apex/__init__.py'
4
adding 'apex/battery.py'
5
adding 'apex-1.0.0.dist-info/METADATA'
6
adding 'apex-1.0.0.dist-info/WHEEL'
7
adding 'apex-1.0.0.dist-info/top_level.txt'
8
adding 'apex-1.0.0.dist-info/RECORD'
9
removing build/bdist.linux-x86_64/wheel
10
Successfully built apex-1.0.0.tar.gz and apex-1.0.0-py3-none-any.whl

The build creates a dist folder which contains the distribution files for your package:

  • apex-1.0.0.tar.gz: the source distribution of the apex package. It contains the source code required to install the package.
  • apex-1.0.0-py3-none-any.whl: the binary distribution of the apex package. You can use the binary distribution to install it directly from the source.

The build process also generates an apex.egg-info folder containing metadata about the package.

1
.
2
├── apex
3
│   ├── battery.py
4
│   └── __init__.py
5
├── apex.egg-info
6
│   ├── dependency_links.txt
7
│   ├── PKG-INFO
8
│   ├── SOURCES.txt
9
│   └── top_level.txt
10
├── dist
11
│   ├── apex-1.0.0-py3-none-any.whl
12
│   └── apex-1.0.0.tar.gz
13
├── pyproject.toml
14
└── README.md

Conclusion

Writing your own Python packages requires dealing with a lot of tools, specifying a lot of metadata, and thinking carefully about your dependencies and target audience. But the reward is great.

If you write useful code and package it properly, people will be able to install it easily and benefit from it.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.