Lesson 11: Packaging

A package is a Python project that can be installed with pip install. Making your project installable means collaborators can use it without copying files into their own directories, and you can pin exact versions for reproducibility.


pyproject.toml

pyproject.toml is the modern, single-file replacement for setup.py, setup.cfg, and requirements.txt. It records metadata, dependencies, and tool configuration in one place.

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "my-physics-project"
version = "0.1.0"
description = "Neutrino flux calculator"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}

dependencies = [
    "numpy>=2.0",
    "scipy>=1.13",
    "matplotlib>=3.9",
    "h5py>=3.11",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "ruff>=0.4",
    "mkdocs>=1.6",
    "mkdocstrings[python]>=0.25",
]

Install the project and its dependencies in editable mode (changes to source take effect immediately without reinstalling):

pip install -e .

Install with development extras:

pip install -e ".[dev]"

Editable installs also mean that import mypackage works from anywhere on your machine while pointing at your actual source files, no more sys.path hacks.


Tool configuration in pyproject.toml

All tool configuration lives in the same file. This is the pattern Prometheus follows for ruff and pytest:

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP"]

[tool.pytest.ini_options]
testpaths = ["tests"]
markers = ["slow: full end-to-end simulation tests"]

Having everything in pyproject.toml means a new contributor can clone the repository, run pip install -e ".[dev]", and have a fully working development environment without reading multiple config files.


Versioning

Follow semantic versioning: MAJOR.MINOR.PATCH.

Bump When
PATCH (0.1.1) Bug fix, no API change
MINOR (0.2.0) New feature, backward compatible
MAJOR (1.0.0) Breaking change

Before a stable release, use 0.x.y to signal that the API may still change. Prometheus is at 1.0.0, meaning its public API is stable and the team commits to not breaking it without a major version bump.


Installing from GitHub

For research code that is not yet on PyPI, collaborators can install directly from the repository:

pip install git+https://github.com/username/my-project.git

Install a specific tag or branch:

pip install git+https://github.com/username/my-project.git@v0.3.0
pip install git+https://github.com/username/my-project.git@feature-branch

This is how most physics collaborators share code before a formal PyPI release.


Publishing to PyPI

When your project is stable and you want anyone to install it with pip install my-project:

pip install build twine
python -m build
twine upload dist/*

build creates a .whl (wheel) and a .tar.gz (source distribution) in dist/. twine upload pushes them to PyPI. You will need a PyPI account and an API token.


Alternative build tools

Prometheus uses Poetry rather than plain setuptools. Poetry manages dependencies and creates a poetry.lock that pins every transitive dependency, making environments fully reproducible. Both approaches produce standard packages; setuptools is simpler to start with.


Further reading

The Python Packaging User Guide is the authoritative reference for everything covered in this lesson and beyond: build backends, lock files, publishing workflows, and dependency management strategies. When you run into packaging questions that go beyond the basics here, this is the right place to look.

Lesson 12 covers the final step: automating lint checks, tests, and documentation deployment in CI so that quality is enforced on every pull request.