Python Packaging Notes
Simple guide on creating Python packages.
Creating a Package
For a package called foobar
, create the following file structure:
my_project/
├── pyproject.toml
├── README.md
└── foobar/
├── __init__.py
└── foobar.py
pyproject.toml
[project]
name = "foobar"
version = "0.0.1"
readme = "README.md"
description = "Example python project"
authors = [
{ name = "Evan Widloski", email = "evan_ex@widloski.com" },
]
license = {text = "GPL-3.0"}
keywords = ["your", "keywords", "go", "here"]
requires-python = ">=3.7"
dependencies = [
"numpy",
# consider specifying version as well
"scikit-image==0.17.2",
]
classifiers = [
"Topic :: Software Development :: Libraries",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
[project.urls]
Homepage = "https://github.com/evidlo/foobar"
Repository = "https://github.com/evidlo/foobar"
Issues = "https://github.com/evidlo/foobar/issues"
[project.scripts]
# if you want your code to be able to run directly from command line
myscript = "foobar.foobar:main"
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages]
# automatically look for subfolders with __init__.py
find = {}
[build-system]
requires = ["setuptools>=59.0.0"]
build-backend = 'setuptools.build_meta'
foobar/__init__.py
# Just an Empty file
# This marks the directory as a python module
foobar/version.py
# We put the version in its own file so the version can be imported, if necessary
__version__="0.0.1"
foobar/foobar.py
some_variable = 123
def main():
print("hello")
README.md
# Foobar
A description of your project
## Quickstart
pip install foobar
Installing and Using
Install your package using pip (inside the package directory):
pip install -e .
If you install with the "editable" option -e
, you can make changes to foobar.py
without reinstalling the package.
We can now run myscript
from the command line:
[evan@blackbox ~] myscript
hello
and import things from the package in Python:
>>> from foobar.foobar import some_variable
>>> some_variable
123
Uploading to PyPi
-
Create a pypi account
-
Build the package
python -m build
-
Upload to pypi with twine
twine upload/dist*
Or use this Makefile:
# Evan Widloski - 2019-03-04
# makefile for building Python projects
.PHONY: dist
dist:
python -m build
.PHONY: pypi
pypi: dist
twine upload dist/*
.PHONY: clean
clean:
rm dist/*
make clean
make pypi
Other Good Practices
Docstrings
All functions should have a docstring that explains what the arguments do and what the function returns. Here is a simple example of a function with a docstring
def fibonacci(n):
"""Return Fibonacci sequence up to n elements
Args:
n (int): number of elements to generate
Returns:
list: fibonacci sequence of length n
"""
sequence = [0, 1]
for i in range(n - 2):
sequence.append(sequence[i - 1] + sequence[i - 2])
return sequence[:n]
See more docstring examples here.
Tests
Tests are an automated way to check that code is working as expected. They are necessary to ensure your changes to a function don't break code elsewhere that depends on that function.
As an example, for all functions in foobar/foobar.py
, there should a corresponding test function in tests/test_foobar.py
.
Here is an example function and its test:
foobar/foobar.py
def fibonacci(n):
"""Return Fibonacci sequence up to n elements
Args:
n (int): number of elements to generate
Returns:
list: fibonacci sequence of length n
"""
sequence = [0, 1]
for i in range(n - 2):
sequence.append(sequence[i - 1] + sequence[i - 2])
return sequence[:n]
tests/test_foobar.py
from foobar.foobar import fibonacci
def test_fibonacci():
sequence = fibonacci(5)
assert sequence == [0, 1, 1, 2, 3], "Incorrect fibonacci sequence"
Run tests like this from the myproject/
folder:
pytest --quiet