Packaging & Distribution
Writing a script that works on your machine is one thing. Turning it into something another developer can pip install, or that you can reuse across five internal projects without copy-pasting files, is another. Packaging is the process of describing your project's metadata, dependencies, and build process in a standard way so that tools like pip and build know how to install, build, and distribute it.
You package a project for a few common reasons: publishing an open-source library on PyPI (the Python Package Index) so anyone can pip install your-package, sharing an internal library across multiple projects or teams without duplicating code, or simply giving your own tool a proper install story instead of a folder of loose .py files.
The modern way: pyproject.toml
For years, Python packaging revolved around a setup.py script (sometimes paired with setup.cfg) that ran arbitrary code to describe your project. That approach is still supported, but the ecosystem has standardized on a single declarative file: pyproject.toml. It is plain TOML (no code execution required to read metadata), tool-agnostic, and understood by pip, build, twine, and virtually every modern Python tool.
pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-awesome-package"
version = "0.1.0"
description = "A small utility library for doing awesome things."
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [
{ name = "Your Name", email = "you@example.com" }
]
dependencies = [
"requests>=2.31,<3",
"click>=8.0",
]
[project.urls]
Homepage = "https://github.com/you/my-awesome-package"The [build-system] table tells pip which tool to invoke to build your project — here hatchling, though setuptools (with a [tool.setuptools] section) is just as common and often the default for older or more complex projects. The [project] table holds the metadata that used to live in setup.py's setup() call: name, version, description, dependencies, and author info.
Field | Purpose |
|---|---|
| The distribution name used on PyPI and by |
| Current release version (see semantic versioning below) |
| Runtime packages your project needs, installed automatically |
| Tools needed to build the package itself (e.g. |
Building a distribution
Once pyproject.toml is in place, the standard build tool turns your source tree into two distributable artifacts: a source distribution (sdist) — essentially your source code plus metadata, tarred up — and a wheel — a prebuilt, ready-to-install package format that pip can unpack directly without running any build steps.
pip install build python -m build
This creates a dist/ folder containing something like my_awesome_package-0.1.0.tar.gz (the sdist) and my_awesome_package-0.1.0-py3-none-any.whl (the wheel). Both are what you upload to PyPI.
Publishing with twine
twine is the standard tool for securely uploading your built distributions to PyPI. It handles authentication and uploads over HTTPS, and is the recommended alternative to letting build tools upload directly.
pip install twine twine upload dist/*
Semantic versioning
Most Python packages follow semantic versioning: MAJOR.MINOR.PATCH, e.g. 2.4.1.
MAJOR — incremented for incompatible/breaking API changes.
MINOR — incremented when you add functionality in a backwards-compatible way.
PATCH — incremented for backwards-compatible bug fixes.