Single-sourcing the package version#
Todo
Update this page for build backends other than setuptools.
There are many techniques to maintain a single source of truth for the version number of your project:
Read the file in
setup.pyand get the version. Example (from pip setup.py):import codecs import os.path def read(rel_path): here = os.path.abspath(os.path.dirname(__file__)) with codecs.open(os.path.join(here, rel_path), 'r') as fp: return fp.read() def get_version(rel_path): for line in read(rel_path).splitlines(): if line.startswith('__version__'): delim = '"' if '"' in line else "'" return line.split(delim)[1] else: raise RuntimeError("Unable to find version string.") setup( ... version=get_version("package/__init__.py") ... )
Note
As of the release of setuptools 46.4.0, one can accomplish the same thing by instead placing the following in the project’s
setup.cfgfile (replacing “package” with the import name of the package):[metadata] version = attr: package.__version__
As of the release of setuptools 61.0.0, one can specify the version dynamically in the project’s
pyproject.tomlfile.[project] name = "package" dynamic = ["version"] [tool.setuptools.dynamic] version = {attr = "package.__version__"}
Please be aware that declarative config indicators, including the
attr:directive, are not supported in parameters tosetup.py.Use an external build tool that either manages updating both locations, or offers an API that both locations can use.
Few tools you could use, in no particular order, and not necessarily complete: bump2version, changes, commitizen, zest.releaser.
Set the value to a
__version__global variable in a dedicated module in your project (e.g.version.py), then havesetup.pyread andexecthe value into a variable.version = {} with open("...sample/version.py") as fp: exec(fp.read(), version) # later on we use: version['__version__']
Example using this technique: warehouse.
Place the value in a simple
VERSIONtext file and have bothsetup.pyand the project code read it.with open(os.path.join(mypackage_root_dir, 'VERSION')) as version_file: version = version_file.read().strip()
An advantage with this technique is that it’s not specific to Python. Any tool can read the version.
Warning
With this approach you must make sure that the
VERSIONfile is included in all your source and binary distributions (e.g. addinclude VERSIONto yourMANIFEST.in).Set the value in
setup.py, and have the project code use theimportlib.metadataAPI to fetch the value at runtime. (importlib.metadatawas introduced in Python 3.8 and is available to older versions as theimportlib-metadataproject.) An installed project’s version can be fetched with the API as follows:import sys if sys.version_info >= (3, 8): from importlib import metadata else: import importlib_metadata as metadata assert metadata.version('pip') == '1.2.0'
Be aware that the
importlib.metadataAPI only knows about what’s in the installation metadata, which is not necessarily the code that’s currently imported.If a project uses this method to fetch its version at runtime, then its
install_requiresvalue needs to be edited to installimportlib-metadataon pre-3.8 versions of Python like so:setup( ... install_requires=[ ... 'importlib-metadata >= 1.0 ; python_version < "3.8"', ... ], ... )
An older (and less efficient) alternative to
importlib.metadatais thepkg_resourcesAPI provided bysetuptools:import pkg_resources assert pkg_resources.get_distribution('pip').version == '1.2.0'
If a project uses
pkg_resourcesto fetch its own version at runtime, thensetuptoolsmust be added to the project’sinstall_requireslist.Example using this technique: setuptools.
Set the value to
__version__insample/__init__.pyand importsampleinsetup.py.import sample setup( ... version=sample.__version__ ... )
Warning
Although this technique is common, beware that it will fail if
sample/__init__.pyimports packages frominstall_requiresdependencies, which will very likely not be installed yet whensetup.pyis run.Keep the version number in the tags of a version control system (Git, Mercurial, etc) instead of in the code, and automatically extract it from there using setuptools_scm.