Versioning#
Whenever a problem occurs—whether you ara filing a bug report on a third-party software or processing a report on your own program—the first thing that you need to know is which version of the softare we are talking about.
It is a widely used convention for Python modules to export a __version__
attribute
to specify the version of a given package. You can try it for yourself on any
third-party Python libray you happen to have installed on your machine, e.g.
lbaldini@nblbaldini:~$ python
Python 3.13.7 (main, Aug 14 2025, 00:00:00) ...
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> np.__version__
'2.3.2'
>>>
Cool. I have numpy
2.3.2 installed.
A simple workflow#
As you might imagine, keeping track of the version of your software boils down to
having a version string hard-coded somewhere in your codebase. This can actually
get trickier than it might seem at a first glance, as the version number is needed
in several different places for slightly different reasons (tags are managed by
git in the first place, but presumably you need __version__
to be defined in
some Python module so that you can expose it; your pyproject.toml
needs to
be version-aware; and you might want to spell out which version the online
documentation refers to—perhaps in the release notes).
Now: whatever strategy you resort to, you need to make sure that the information you disseminate in the wild is self-consistent. One dead-simple line of action might be:
keep the
__version__
attribute into a_version.py
file;have a (Python) script tagging your package and updating
_version.py
whenever you want to make a release;have
pyproject.toml
read the version from_version.py
;expose the
__version__
attribute in your package’s__init__.py
file.
The pyproject.toml
magic is easily done via something along the lines of
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
dynamic = ["version"]
[tool.hatch.version]
path = "src/metarep/_version.py"
And in the top-level __init__.py
file of your package you might be tempted
to simply write
from ._version import __version__
Sure enough, when you install the package (e.g., via pip) you will get the same
ergonomics as any other third-party library. One slight problem with this simplistic
approach is that if you are actively developing a project in a working copy of a
git repo, the version string will not generally capture local modifications—you
will keep getting the same version until you make a new tag. You might care or not
about this, but this is something that can be easily fixed with a slightly
more complicated __init__.py
file, e.g.,
# Copyright (C) 2025 Luca Baldini (luca.baldini@pi.infn.it)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import pathlib
import subprocess
from ._version import __version__ as __base_version__
def _git_suffix() -> str:
"""If we are in a git repo, we want to add the necessary information to the
version string.
This will return something along the lines of ``+gf0f18e6.dirty``.
"""
# pylint: disable=broad-except
kwargs = dict(cwd=pathlib.Path(__file__).parent, stderr=subprocess.DEVNULL)
try:
# Retrieve the git short sha to be appended to the base version string.
args = ["git", "rev-parse", "--short", "HEAD"]
sha = subprocess.check_output(args, **kwargs).decode().strip()
suffix = f"+g{sha}"
# If we have uncommitted changes, append a `.dirty` to the version suffix.
args = ["git", "diff", "--quiet"]
if subprocess.call(args, stdout=subprocess.DEVNULL, **kwargs) != 0:
suffix = f"{suffix}.dirty"
return suffix
except Exception:
return ""
__version__ = f"{__base_version__}{_git_suffix()}"