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 |
|
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 fieldsname
andemail
for 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.y
versions 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
twine
with--repository-url
1 |
|
GitLab or GitHub Package Registries⚓
- GitLab: Packages & Registries
- GitHub: Publishing Python packages