Skip to content

Packaging and Releasing Python Projects

Python’s ecosystem provides a robust toolchain to turn your code into reusable and shareable packages. Packaging enables:

  • Dependency reuse (e.g., via pip)
  • Easy installation via PyPI or private indexes
  • Versioned releases and changelog tracking
  • Integration with CI/CD for automated distribution

Key Tools

Tool Purpose
setuptools Core tool for describing and building packages
build Builds sdist and wheel from your project
twine Securely uploads packages to PyPI

Project Structure

A typical modern Python project might look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
cookie-factory/
├─── docs
├─── src
|    ├─── cookie_factory
|    |    ├─── __init__.py
|    |    ├─── __main__.py
|    |    ├─── cookie.py
|    |    └─── factory.py
├─── tests
|    ├─── test_cookie.py
│    └─── test_factory.py
├─── pyproject.toml
├─── README.md
├─── LICENSE
├─── CHANGELOG.md
└─── .gitignore

For packaging and publishing your package though, you at least need the following structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cookie-factory/
├─── src
|    ├─── cookie_factory
|    |    ├─── __init__.py
|    |    ├─── __main__.py
|    |    ├─── cookie.py
|    |    └─── factory.py
├─── pyproject.toml
├─── README.md
└─── LICENSE

Project Metadata

The pyproject.toml file is the modern standard for Python packaging metadata and build configuration. It defines your project, its tools, dependencies, build configuration, etc and is organized in multiple sections.

Project Section

This section gives metadata describing your project. Here is a non-exhaustive list of information you can add to the section:

  • name: the name of your project, it corresponds to the name it will have on PyPI once published
  • version: the current version of your project, this number should only be incremented and respect Semantic Versioning
  • description: a short description of your package
  • readme: the name of the readme file, will be included in your package build and publication (main page for your package on PyPI)
  • authors: a list containing the package authors, with fields name and email for each
  • license: the license for your software, can be the license file directly

This section can also be split in subsections. project.urls allows you to give the URL for your projects. They will be displayed on PyPI left navigation bar.

It also contains your package requirements:

  • requires-python: what version of python is needed for your project, can be a single one (e.g "==3.12"), or a condition (e.g ">=3.9")
  • dependencies: the list of package dependencies, can contain package names and optionally conditions on the package versions (e.g "requests >=2.28")

Note

To help your package stand out in the jungle of all packages published on PyPI, you could also provide classifiers. These are tags applied to your package, to categorize it.

Build System Section

This section contains settings to build your package. Specifically, it contains at least the following information:

  • requires: the build-time requirements of your project
  • build-backend: the tool used to build your package (e.g "setuptools.build_meta" for setuptools)

Complete example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "cookie-factory"
version = "0.1.0"
description = "A cool Python package"
readme = "README.md"
requires-python = ">=3.9"
authors = [
  { name = "Your Name", email = "your@email.com" }
]
license = { file = "LICENSE" }
dependencies = [
  "requests >=2.28",
]

[project.urls]
Homepage = "https://gilab.com/yourname/myproject"
Source = "https://gilab.com/yourname/myproject"

For more detailed documentation: PEP 621

Building the Package

Once your pyproject.toml file is setup correctly, as the rest of the project structure, you can build it.

Install build tool

The standard build tool for python packages is the build package. You can install it easily:

1
pip install build

Note

If you are using a project tool that can also handle the building operation, you can refer to its own documentation.

Build the packages

Build the source (sdist) and wheel (.whl) distributions of your package:

1
python -m build

This creates:

1
2
3
dist
├── cookie-factory-0.1.0.tar.gz            # source distribution
└── cookie-factory-0.1.0-py3-none-any.whl  # wheel (binary) distribution

Uploading the package

Install twine

twine is a tool which handles the publication of your package to PyPI or other package registry:

1
pip install twine

(Optional) Test upload to TestPyPI

TestPyPI is another instance of PyPI, which you can use to test your package publication process.

1
twine upload --repository testpypi dist/*

Real upload to PyPI

Once you are confident of your package build, you can upload it to PyPI:

1
twine upload dist/*

You'll be prompted for your PyPI credentials. You can also configure them via .pypirc.

Note

The first time you are uploading your package, make sure its name is not taken by another package. You also need an account on PyPI (it will ask you to setup 2FA with an authenticator app).

You can automatize this process in the CI/CD, you can use one of 2 methods to authenticate your pipelines for upload:

  • Generate an API token for your PyPI or TestPyPI account (or even better, for the specifc package), and use it as a password (and __token__ as the username)
  • Set up a Trusted Publisher by linking your package on PyPI or TestPyPI with your online Git Repository

Semantic Versioning (SemVer)

Semantic Versioning is a widely adopted standard for versioning software, defined at semver.org. It provides a clear and predictable way to communicate changes in your software to users and developers.

Version Format

A semantic version has the format:

1
MAJOR.MINOR.PATCH

Each part is a number, which can only be incremented.

For example:

1
2.3.1

This means:

  • 2 → MAJOR version
  • 3 → MINOR version
  • 1 → PATCH version

Note

There can be other parts to a semantic version number, adding project status such as pre-release, alpha or else. Unless you have setup a complete CI/CD pipeline with very fast deployment cycle, you should not need those. Otherwise, check semver.org for more documentation.

What Triggers a Version Change?

Type of Change Increment Example Before → After
Breaking change MAJOR 1.2.3 → 2.0.0
Backward-compatible new feature MINOR 1.2.3 → 1.3.0
Bug fix only PATCH 1.2.3 → 1.2.4

💡 Rule of thumb: Any change in public API behavior requires a version bump.

Example Cases

Change Made Version Bump
Refactored internal logic without changing public API No change or PATCH (if bug fixed)
Added a new optional argument to a public function MINOR
Removed or renamed a public function MAJOR
Fixed an off-by-one error in result output PATCH
Rewrote the module using a different backend, but API is same MINOR (if backward-compatible) or MAJOR (if subtle behavior change)
Added a CLI entrypoint or command-line option MINOR
Added type hints (Python 3) PATCH or MINOR (depending on impact)

Pre-1.0.0 Considerations

According to SemVer, 0.x.y versions are considered unstable and may change at any time.

If your project is in early development:

  • 0.1.0: Initial public draft
  • 0.2.0: Any backward-incompatible change
  • 0.2.1: Bug fix

Once your API is stable and you intend to support backward compatibility, you should release 1.0.0.

Using SemVer in Python Projects

In your pyproject.toml:

1
2
[project]
version = "1.3.0"

You can also store the version in a single __version__ variable in your package (e.g., cookie-factory/src/cookie_factory/__init__.py) and refer to it dynamically.

1
__version__ = "1.3.0"

Best Practices

  • Tag every release in Git:
1
2
git tag v1.3.0
git push origin v1.3.0
  • Keep a CHANGELOG.md and relate changes to version bumps.

  • Align CI/CD deployments (e.g., PyPI, Docker) with SemVer tags.

  • Automate bumping with tools like bump2version or uv.

For more documentation see:

Keeping a CHANGELOG.md

A changelog is a human-readable file that documents all notable changes between versions of a project. It helps users, developers, and contributors understand what changed, when, and why.

Why Maintain a Changelog?

  • 📦 Communicate changes to your users
  • 🚨 Help users identify breaking changes
  • 🛠 Track bug fixes, new features, and improvements
  • 🔄 Support rollback decisions in case of regressions
  • 🧪 Assist testers and QA in validating releases

Changelogs are often required for production-ready software, especially when following Semantic Versioning.

Where to Put It

Create a top-level CHANGELOG.md file in your repository root.

This file is often included in documentation and packaging metadata.

Typical Changelog Structure

Use reverse chronological order with clear version headers and dates:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Changelog

All notable changes to this project will be documented in this file.

## [1.3.0] - 2025-08-01
### Added
- New `--dry-run` flag in CLI for testing commands
- Support for reading config from environment variables

### Changed
- CLI now exits with code `1` on failure instead of `0`
- Updated dependencies to latest minor versions

### Fixed
- Bug in API client when handling 204 No Content responses

## [1.2.1] - 2025-07-15
### Fixed
- Crash when parsing empty config files

Use consistent headings:

  • Added – for new features
  • Changed – for changes in existing functionality
  • Deprecated – for soon-to-be removed features
  • Removed – for now-removed features
  • Fixed – for bug fixes
  • Security – in case of vulnerabilities

Best Practices

  • Keep it brief but informative
  • Link to pull requests and issues if helpful
  • Update it before each release (automated in CI/CD is ideal)
  • Follow the Keep a Changelog format if you need a standard

Private/Internal Repositories

If you work in an enterprise or lab setting, you may want to publish internally:

Self-hosted Index with pypiserver or devpi

  • Run a local PyPI-compatible server
  • Upload using twine with --repository-url
1
twine upload --repository-url https://mycompany/pypi/ dist/*

GitLab or GitHub Package Registries