PythonPackaging & Distribution

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

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

name

The distribution name used on PyPI and by pip install

version

Current release version (see semantic versioning below)

dependencies

Runtime packages your project needs, installed automatically

build-system.requires

Tools needed to build the package itself (e.g. hatchling, setuptools)

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.

Bash
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.

Bash
pip install twine
twine upload dist/*
Practice on TestPyPI first
`https://test.pypi.org` is a separate, fully functional instance of PyPI meant for practice releases. Upload there first with `twine upload --repository testpypi dist/*` to make sure your metadata, README rendering, and install process work before you commit to a real, permanent release on the main index.
PyPI releases are (mostly) forever
You cannot re-upload a file for a version number that has already been published, and deleting a release does not free up that version number again. Get comfortable with a test release before publishing for real.
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.

Why this matters
Consumers of your package can pin version ranges like `my-awesome-package>=2.0,<3.0` and trust that they will get bug fixes and new features without breaking changes sneaking in.