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

License and Copyrights

Choose a License

Before publishing your python software, you should decide on which license to use.

We recommend the choice of an open source license, and one which is under the jurisdiction of your country (easier to uphold in cases of problems later on). The EU proposes the EUPL license, which is compatible with most mainstream open source licenses (MIT, BSD, MPL, Apache, etc), and especially its newest iteration: EUPL v1.2.

Note

You can check the compatibility of the EUPL v1.2 license with other software licenses here.

This license must be compatible with the external software you are using (this means copying pieces of code, importing functionalities from external python package).

The full license text shall be included in your package (e.g as LICENSE.txt or LICENSE).

The following section is not legal advice, but a summary of licensing rules applied to python.

You have different cases when using functionalities/code from external sources:

  • importing them at runtime, adding them to your requirements only: then your software contains no external source code and is simply an instruction to combine those requirements with your own code to make a final software, in that case you should be able to choose the license however you want (though you may be pushing the licenses verification downstream to every user who want to incorporate your work)
  • incorporating pieces of code, modified or not, to your own software: in that case, the rules of incorporation of code applies, and you should check the external source code licenses and see what you can and can't do, and under which conditions
  • you are compiling your python software and the compilation bundle your code with external one in the binaries: in that case, you are statically linking your software with those dependencies, you should check the dependencies licenses as above
  • you are distributing a built Docker image of your project, in which the dependencies are installed (e.g through pip): then the dependencies are incorporated into that image, you need to check as above the rules for incorporation

Warning

You should be very wary of dependencies with GPL licenses, even for simple dependencies only imported and mentionned in requirements. They are very strict, and even if you technically can use them, you are strongly limiting how others can reuse your software to build new one. Also its terms and conditions might not apply in the EU context. This is not the case for LGPL though, as it is more permissive.

Copyrights notice

When developing software professionaly, this software belongs to the company or organization employing you. Even in an academic context, code developed belong to your employer. You are however the author of the software and shall be recognized as such.

Therefore, you should include those copyrights as a header in your project, they should mention at least the following information:

  • Copyrights holder
  • Years of the project (at least start date of the copyrights)
  • License type chosen
  • (Authors and contributors optionnaly)
  • (you could add a quick software intent, though it may already be present elsewhere, e.g in the README)
  • (license notice)

Here is a template of such copyright notice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Copyright IMT Atlantique, <start year of project>

Project leaders:

<list leaders/authors by name with contact information (i.e email)>

This software is <explain intent>.

This software is licensed under the <full name of license>.

<include license notice>

For EUPL v1.2, here is the license notice you can include below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Licensed under the EUPL, Version 1.2 or – as soon they
will be approved by the European Commission - subsequent
versions of the EUPL (the "Licence");
You may not use this work except in compliance with the
Licence.
You may obtain a copy of the Licence at:

https://joinup.ec.europa.eu/page/eupl-text-11-12

Unless required by applicable law or agreed to in
writing, software distributed under the Licence is
distributed on an "AS IS" basis,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied.
See the Licence for the specific language governing
permissions and limitations under the Licence.

Those information should at least be contained in a section of the README. If you are publishing a full documentation, its home page should also include the copyright notice.

Note

In theory, you should also include it as a Multi-Line Block Comment into each source file you of your project. It's mainly useful if you want to ease the task of developers including some of your source code as-is in their project (as the copyright header will already be included in the copy).

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