Packaging a single module in Python

Are you tired of endless __init__.py in your code?

George Shuklin
OpsOps

--

At recent code review my colleague pointed out to a lot of code inside __init__.py. I looked at those things with fresh eye and found, that many things I done in the past is attempt to bend a complex thing to be simple when there is a easily available simple thing at the first place.

Can modules be packaged?

If you ever touched packaging for Python, it’s overwhelming. You deal with this by learning few magic tricks which works most of the time and cause pain in some cases. But as it turned out, all that complexity is for complex cases.

If you have a simple case, you don’t need even get closer to all that.

My main mistake was that I assumed that only packages can be packaged, so one NEED to make one. (A package is a folder with __init__.py and a bunch of modules. A module is any some_name.py file.)

Python package and pipy package are different thing!

pip (the sane way to install python packages) operates on package level. So, there is an assumption you need to create a Python package to use it with pip.

Wrong!

There is a bitter clash here. Packages you redistribute with pip may contain a single module. No __init__.py, no complicated reexports, etc.

I have no qualification to explain whole packing thing (all those egg/wheels are beyond the reasonable curiosity), but I’ll show you the minimal, clean way to create a package with a single module.

Directory structure

└── normal
├── setup.py
├── test_normal.py
└── normal.py

That’s it. No nested directories, no __init__.py anywhere.

Details

test_normal.py can just import normal. No directory traversal of any kind.

Content of the setup.py (oversimplified):

from setuptools import setup

setup(
name="normal",
version="1.0.0",
py_modules=["normal"]
)

Throw in additional fields as you need. The key magic here is py_modules=["normal"] (notice, there is no .py).

After you’ve make it (suspiciously easy), it can be installed:

pip install normalBuilding wheel for normal (setup.py) ... done
Created wheel for normal: filename=normal-1.0.0-py3-none-any.whl size=3313 sha256=ae8c401e56d4e93da824a9550bf78bc985de78269ea22de76374c6f49e280594
Stored in directory: /tmp/pip-ephem-wheel-cache-za1cyvld/wheels/cc/76/8f/ 28cd5c16a6b3e0e1ea7968abba7a40542bee6218b1b9532c6c
Successfully built normal
Installing collected packages: normal
Successfully installed normal-1.0.0

What it creates in a target system? Again, too simple to be true…

.../site-packages/normal-1.0.0.dist-info/*
.../site-packages/normal.py

That’s all? Yep.

Can we use it? Yep.

>>> import normal
>>> dir(normal)
['Normal', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']

I can’t believe I struggle so much for so many years over-complicating things which are so simple…

--

--

George Shuklin
OpsOps

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux. My hobby is Rust.