4.3 Software Packaging and Release
Last updated on 2025-11-17 | Edit this page
Overview
Questions
- How do we prepare our code for sharing as a Python package?
- How do we release our project for other people to install and reuse?
Objectives
- Describe the steps necessary for sharing Python code as installable packages.
- Use
uvto prepare an installable package. - Explain the differences between runtime and development dependencies.
We have now got our software ready to release. All the changes were
made in the main branch. The last step is to give it a
version number and find a proper way to distribute it. We will look at
how to make a release on GitHub, then we will look at how to package our
code so that others can easily install and use it.
Update the Version Number in pyproject.toml
We start on the main branch.
First we need to update the version number in our code. This is
typically done in the pyproject.toml for Python projects.
We currently have the version set to 0.0.0, so let us
update it to 0.1.0:
Then we need to commit this change to the main
branch:
Note that this is usually done on a feature branch and then merged
into main with a pull request.
What is a Version Number Anyway?
Software version numbers are everywhere, and there are many different ways to do it. A popular one to consider is Semantic Versioning, where a given version number uses the format MAJOR.MINOR.PATCH. You increment the:
- MAJOR version when you make incompatible API changes
- MINOR version when you add functionality in a backwards compatible manner
- PATCH version when you make backwards compatible bug fixes
You can also add a hyphen followed by characters to denote a pre-release version, e.g. 1.0.0-alpha1 (first alpha release) or 1.2.3-beta4 (fourth beta release)
Tagging a Release in GitHub
There are many ways in which Git and GitHub can help us make a software release from our code. For example, we can use GitHub website to create a new release. In this episode, we will look at how to do this using Git tagging at the command line.
Let us see what tags we currently have in our repository:
Since we have not tagged any commits yet, there is unsurprisingly no
output. We can create a new tag on the last commit in our
main branch by doing:
So we can check the tags again:
A tag should now be listed:
OUTPUT
0.1.0
And also, for more information:
So now we have added a tag, we need this reflected in our Github repository. You can push this tag to your remote by doing:
We can now use the more memorable tag to refer to this specific
commit. Plus, once we have pushed this back up to GitHub, it appears as
a specific release within our code repository which can be downloaded in
compressed .zip or .tar.gz formats. Note that
these downloads just contain the state of the repository at that commit,
and not its entire history.
Using tagging allows us to highlight commits that are particularly important, which is very useful for reproducibility purposes. We can (and should) refer to specific commits for software in academic papers that make use of results from software, but tagging with a specific version number makes that just a little bit easier for humans.
Packaging our Software with uv
For very small pieces of software, for example a single source file, it may be appropriate to distribute to non-technical end-users as source code, but in most cases we want to bundle our application or library into a package. A package is typically a single file which contains within it our software and some metadata which allows it to be installed and used more simply - e.g. a list of dependencies. By distributing our code as a package, we reduce the complexity of fetching, installing and integrating it for the end-users.
Further reading: Python Packaging User Guide
You can refer to the Python Packaging User Guide for documentation on best practices and tools for packaging Python projects.
At the end of this episode, there is an optional exercise where you can try more good practices for packaging your Python project
For packaging our code, we will introduce uv, an
extremely fast Python package and project manager, written in Rust.
Installing uv
On MacOS or Linux, you can install uv using the
following command:
On Windows, you can install uv by:
You can refer to the uv installation documentation for more details on installation.
Packaging Our Code
The final preparation we need to do is to make sure that our code is
organised in the recommended structure. This is the Python module
structure - a directory containing an __init__.py and our
Python source code files. Make sure that the name of this directory is
inflammation, which matches the following section in
pyproject.toml:
Then “inflammation” will be the name of the package when it is
installed. Of course you can choose different names for your package,
but you must ensure that the pyproject.toml file is updated
accordingly.
Once we have made sure our project is in the right structure, we can go ahead and build a distributable version of our software:
This should produce two files in the dist/
directory:
OUTPUT
dist/python_intermediate_inflammation-0.1.0.tar.gz
dist/python_intermediate_inflammation-0.1.0-py3-none-any.whl
The one we care most about is the .whl or
wheel file. This is the file that pip uses
to distribute and install Python packages, so this is the file we would
need to share with other people who want to install our software.
By convention distributable package names use hyphens, whereas module package names use underscores. While we could choose to use underscores in a distributable package name, we cannot use hyphens in a module package name, as Python will interpret them as a minus sign in our code when we try to import them.
Now if we gave this wheel file to someone else, they could install it
using pip (you do not need to run this command
yourself)
And then they could import our package in their own Python environment:
After we have been working on our code for a while and want to
publish an update, we just need to update the version number in the
pyproject.toml file (using SemVer perhaps), then use uv
to build and publish the new version.
uv can help easily increment the version number
following Semantic Versioning conventions. For example, to increment the
minor version number, we can do:
Then the version number in pyproject.toml will be
updated from 0.1.0 to 0.2.0.
For more information about versioning with uv, see the
uv
versioning documentation.
Project Dependencies
Tools like uv understand that there are two different
types of dependency: runtime dependencies and development dependencies.
Runtime dependencies are those dependencies that need to be installed
for our code to run, like numpy. Development dependencies
are dependencies which are an essential part of your development process
for a project, but are not required to run it. Like mkdocs
we used to build our documentation.
When we add a dependency using uv, uv will
add it to the list of dependencies in the pyproject.toml
file, and automatically install the package into our virtual
environment, even if the virtual environment is not currently
activated.
For example, one can add numpy matplotlib as a runtime
dependency by doing:
This will add numpy and matplotlib to the
list of runtime dependencies in pyproject.toml:
TOML
[project]
name = "python-intermediate-inflammation"
version = "0.2.0"
requires-python = ">=3.9"
dependencies = [
"matplotlib>=3.9.4",
"numpy>=2.0.2",
]
This is an alternative way to specify the dependencies than the
requirements.txt file we created before. The advantage of
specifying dependencies in pyproject.toml, is that it
centralizes this information in one place, and we can also make a
distinction between runtime and development dependencies.
To add mkdocs as a development dependency, the
--group option can be used:
This will add a new section to the pyproject.toml file
for development dependencies:
By default, when someone installs our package using pip,
only the runtime dependencies will be installed, as development
dependencies are not needed to run the code.
To install the development dependencies, one need to clone our
repository from GitHub and then specify the dev extra when
installing:
This behavior can be customized in the pyproject.toml
file. Check the uv
documentation on dependencies
What if We Need More Control?
There many ways to distribute Python code in packages, with some degree of flux in terms of which methods are most popular. For a more comprehensive overview of Python packaging you can see the Python docs on packaging, which contains a helpful guide to the overall packaging process, or ‘flow’, using the Twine tool to upload created packages to PyPI for distribution as an alternative.
Optional Exercise: Enhancing our Package Metadata
The Python Packaging User
Guide provides documentation on how
to package a project using a manual approach to building a
pyproject.toml file, and using Twine to upload the
distribution packages to PyPI.
Referring to the section
on metadata in the documentation, enhance your
pyproject.toml with some additional metadata fields to
improve the information your package.
-
uvallows us to produce an installable package and upload it to a package repository. - Making our software installable with Pip makes it easier for others to start using it.
- For complete control over building a package, we can use a
setup.pyfile.