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 | |
For packaging and publishing your package though, you at least need the following structure:
1 2 3 4 5 6 7 8 9 10 | |
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 | |
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 | |
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 publishedversion: the current version of your project, this number should only be incremented and respect Semantic Versioningdescription: a short description of your packagereadme: 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 fieldsnameandemailfor eachlicense: 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 projectbuild-backend: the tool used to build your package (e.g"setuptools.build_meta"forsetuptools)
Complete example⚓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
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 | |
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 | |
This creates:
1 2 3 | |
Uploading the package⚓
Install twine⚓
twine is a tool which handles the publication of your package to PyPI or other package registry:
1 | |
(Optional) Test upload to TestPyPI⚓
TestPyPI is another instance of PyPI, which you can use to test your package publication process.
1 | |
Real upload to PyPI⚓
Once you are confident of your package build, you can upload it to PyPI:
1 | |
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 | |
Each part is a number, which can only be incremented.
For example:
1 | |
This means:
2→ MAJOR version3→ MINOR version1→ 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.yversions are considered unstable and may change at any time.
If your project is in early development:
0.1.0: Initial public draft0.2.0: Any backward-incompatible change0.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 | |
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 | |
Best Practices⚓
- Tag every release in Git:
1 2 | |
-
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:
- Official SemVer spec: https://semver.org
- Semantic versioning in PyPI: PEP 440
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 | |
Recommended Categories⚓
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
twinewith--repository-url
1 | |
GitLab or GitHub Package Registries⚓
- GitLab: Packages & Registries
- GitHub: Publishing Python packages