Standard way to embed version into Python package?

Viewed 228196

Is there a standard way to associate version string with a Python package in such way that I could do the following?

import foo
print(foo.version)

I would imagine there's some way to retrieve that data without any extra hardcoding, since minor/major strings are specified in setup.py already. Alternative solution that I found was to have import __version__ in my foo/__init__.py and then have __version__.py generated by setup.py.

21 Answers

Not directly an answer to your question, but you should consider naming it __version__, not version.

This is almost a quasi-standard. Many modules in the standard library use __version__, and this is also used in lots of 3rd-party modules, so it's the quasi-standard.

Usually, __version__ is a string, but sometimes it's also a float or tuple.

As mentioned by S.Lott (Thank you!), PEP 8 says it explicitly:

Module Level Dunder Names

Module level "dunders" (i.e. names with two leading and two trailing underscores) such as __all__, __author__, __version__, etc. should be placed after the module docstring but before any import statements except from __future__ imports.

You should also make sure that the version number conforms to the format described in PEP 440 (PEP 386 a previous version of this standard).

There is a slightly simpler alternative to some of the other answers:

__version_info__ = ('1', '2', '3')
__version__ = '.'.join(__version_info__)

(And it would be fairly simple to convert auto-incrementing portions of version numbers to a string using str().)

Of course, from what I've seen, people tend to use something like the previously-mentioned version when using __version_info__, and as such store it as a tuple of ints; however, I don't quite see the point in doing so, as I doubt there are situations where you would perform mathematical operations such as addition and subtraction on portions of version numbers for any purpose besides curiosity or auto-incrementation (and even then, int() and str() can be used fairly easily). (On the other hand, there is the possibility of someone else's code expecting a numerical tuple rather than a string tuple and thus failing.)

This is, of course, my own view, and I would gladly like others' input on using a numerical tuple.


As shezi reminded me, (lexical) comparisons of number strings do not necessarily have the same result as direct numerical comparisons; leading zeroes would be required to provide for that. So in the end, storing __version_info__ (or whatever it would be called) as a tuple of integer values would allow for more efficient version comparisons.

Lots of work toward uniform versioning and in support of conventions has been completed since this question was first asked. Palatable options are now detailed in the Python Packaging User Guide. Also noteworthy is that version number schemes are relatively strict in Python per PEP 440, and so keeping things sane is critical if your package will be released to the Cheese Shop.

Here's a shortened breakdown of versioning options:

  1. Read the file in setup.py (setuptools) and get the version.
  2. Use an external build tool (to update both __init__.py as well as source control), e.g. bump2version, changes or zest.releaser.
  3. Set the value to a __version__ global variable in a specific module.
  4. Place the value in a simple VERSION text file for both setup.py and code to read.
  5. Set the value via a setup.py release, and use importlib.metadata to pick it up at runtime. (Warning, there are pre-3.8 and post-3.8 versions.)
  6. Set the value to __version__ in sample/__init__.py and import sample in setup.py.
  7. Use setuptools_scm to extract versioning from source control so that it's the canonical reference, not code.

NOTE that (7) might be the most modern approach (build metadata is independent of code, published by automation). Also NOTE that if setup is used for package release that a simple python3 setup.py --version will report the version directly.

Also worth noting is that as well as __version__ being a semi-std. in python so is __version_info__ which is a tuple, in the simple cases you can just do something like:

__version__ = '1.2.3'
__version_info__ = tuple([ int(num) for num in __version__.split('.')])

...and you can get the __version__ string from a file, or whatever.

arrow handles it in an interesting way.

Now (since 2e5031b)

In arrow/__init__.py:

__version__ = 'x.y.z'

In setup.py:

from arrow import __version__

setup(
    name='arrow',
    version=__version__,
    # [...]
)

Before

In arrow/__init__.py:

__version__ = 'x.y.z'
VERSION = __version__

In setup.py:

def grep(attrname):
    pattern = r"{0}\W*=\W*'([^']+)'".format(attrname)
    strval, = re.findall(pattern, file_text)
    return strval

file_text = read(fpath('arrow/__init__.py'))

setup(
    name='arrow',
    version=grep('__version__'),
    # [...]
)

There doesn't seem to be a standard way to embed a version string in a python package. Most packages I've seen use some variant of your solution, i.e. eitner

  1. Embed the version in setup.py and have setup.py generate a module (e.g. version.py) containing only version info, that's imported by your package, or

  2. The reverse: put the version info in your package itself, and import that to set the version in setup.py

Using setuptools and pbr

There is not a standard way to manage version, but the standard way to manage your packages is setuptools.

The best solution I've found overall for managing version is to use setuptools with the pbr extension. This is now my standard way of managing version.

Setting up your project for full packaging may be overkill for simple projects, but if you need to manage version, you are probably at the right level to just set everything up. Doing so also makes your package releasable at PyPi so everyone can download and use it with Pip.

PBR moves most metadata out of the setup.py tools and into a setup.cfg file that is then used as a source for most metadata, which can include version. This allows the metadata to be packaged into an executable using something like pyinstaller if needed (if so, you will probably need this info), and separates the metadata from the other package management/setup scripts. You can directly update the version string in setup.cfg manually, and it will be pulled into the *.egg-info folder when building your package releases. Your scripts can then access the version from the metadata using various methods (these processes are outlined in sections below).

When using Git for VCS/SCM, this setup is even better, as it will pull in a lot of the metadata from Git so that your repo can be your primary source of truth for some of the metadata, including version, authors, changelogs, etc. For version specifically, it will create a version string for the current commit based on git tags in the repo.

As PBR will pull version, author, changelog and other info directly from your git repo, so some of the metadata in setup.cfg can be left out and auto generated whenever a distribution is created for your package (using setup.py)



Get the current version in real-time

setuptools will pull the latest info in real-time using setup.py:

python setup.py --version

This will pull the latest version either from the setup.cfg file, or from the git repo, based on the latest commit that was made and tags that exist in the repo. This command doesn't update the version in a distribution though.



Updating the version metadata

When you create a distribution with setup.py (i.e. py setup.py sdist, for example), then all the current info will be extracted and stored in the distribution. This essentially runs the setup.py --version command and then stores that version info into the package.egg-info folder in a set of files that store distribution metadata.

Note on process to update version meta-data:

If you are not using pbr to pull version data from git, then just update your setup.cfg directly with new version info (easy enough, but make sure this is a standard part of your release process).

If you are using git, and you don't need to create a source or binary distribution (using python setup.py sdist or one of the python setup.py bdist_xxx commands) the simplest way to update the git repo info into your <mypackage>.egg-info metadata folder is to just run the python setup.py install command. This will run all the PBR functions related to pulling metadata from the git repo and update your local .egg-info folder, install script executables for any entry-points you have defined, and other functions you can see from the output when you run this command.

Note that the .egg-info folder is generally excluded from being stored in the git repo itself in standard Python .gitignore files (such as from Gitignore.IO), as it can be generated from your source. If it is excluded, make sure you have a standard "release process" to get the metadata updated locally before release, and any package you upload to PyPi.org or otherwise distribute must include this data to have the correct version. If you want the Git repo to contain this info, you can exclude specific files from being ignored (i.e. add !*.egg-info/PKG_INFO to .gitignore)



Accessing the version from a script

You can access the metadata from the current build within Python scripts in the package itself. For version, for example, there are several ways to do this I have found so far:

## This one is a new built-in as of Python 3.8.0 should become the standard
from importlib.metadata import version

v0 = version("mypackage")
print('v0 {}'.format(v0))

## I don't like this one because the version method is hidden
import pkg_resources  # part of setuptools

v1 = pkg_resources.require("mypackage")[0].version
print('v1 {}'.format(v1))

# Probably best for pre v3.8.0 - the output without .version is just a longer string with
# both the package name, a space, and the version string
import pkg_resources  # part of setuptools

v2 = pkg_resources.get_distribution('mypackage').version
print('v2 {}'.format(v2))

## This one seems to be slower, and with pyinstaller makes the exe a lot bigger
from pbr.version import VersionInfo

v3 = VersionInfo('mypackage').release_string()
print('v3 {}'.format(v3))

You can put one of these directly in your __init__.py for the package to extract the version info as follows, similar to some other answers:

__all__ = (
    '__version__',
    'my_package_name'
)

import pkg_resources  # part of setuptools

__version__ = pkg_resources.get_distribution("mypackage").version

After several hours of trying to find the simplest reliable solution, here are the parts:

create a version.py file INSIDE the folder of your package "/mypackage":

# Store the version here so:
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = '1.2.7'

in setup.py:

exec(open('mypackage/version.py').read())
setup(
    name='mypackage',
    version=__version__,

in the main folder init.py:

from .version import __version__

The exec() function runs the script outside of any imports, since setup.py is run before the module can be imported. You still only need to manage the version number in one file in one place, but unfortunately it is not in setup.py. (that's the downside, but having no import bugs is the upside)

pbr with bump2version

This solution was derived from this article

The use case - python GUI package distributed via PyInstaller. Needs to show version info.

Here is the structure of the project packagex

packagex
├── packagex
│   ├── __init__.py
│   ├── main.py
│   └── _version.py
├── packagex.spec
├── LICENSE
├── README.md
├── .bumpversion.cfg
├── requirements.txt
├── setup.cfg
└── setup.py

where setup.py is

# setup.py
import os

import setuptools

about = {}
with open("packagex/_version.py") as f:
    exec(f.read(), about)

os.environ["PBR_VERSION"] = about["__version__"]

setuptools.setup(
    setup_requires=["pbr"],
    pbr=True,
    version=about["__version__"],
)

packagex/_version.py contains just

__version__ = "0.0.1"

and packagex/__init__.py

from ._version import __version__

and for .bumpversion.cfg

[bumpversion]
current_version = 0.0.1
commit = False
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+)(?P<build>\d+))?
serialize = 
    {major}.{minor}.{patch}-{release}{build}
    {major}.{minor}.{patch}

[bumpversion:part:release]
optional_value = prod
first_value = dev
values = 
    dev
    prod

[bumpversion:file:packagex/_version.py]

I prefer to read the package version from installation environment. This is my src/foo/_version.py:

from pkg_resources import get_distribution                                        
                                                                                  
__version__ = get_distribution('foo').version

Makesure foo is always already installed, that's why a src/ layer is required to prevent foo imported without installation.

In the setup.py, I use setuptools-scm to generate the version automatically.


Update in 2022.7.5:

There is another way, which is my faviourate now. Use setuptools-scm to generate a _version.py file.

setup(
    ...
    use_scm_version={
        'write_to':
        'src/foo/_version.py',
        'write_to_template':
        '"""Generated version file."""\n'
        '__version__ = "{version}"\n',
    },
)
  1. Create a file named by _version.txt in the same folder as __init__.py and write version as a single line:
0.8.2
  1. Read this infomation from file _version.txt in __init__.py:
    import os 
    def get_version():
        with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "_version.txt")) as f:
            return f.read().strip() 
    __version__ = get_version()

I described a standard and modern way here, relying on setuptools_scm. This pattern has worked successfully for dozens of published packages over the past years, so I can warmly recommend it.

Note that you do not need the getversion package to implement this pattern. It just happens that the getversion documentation hosts this tip.

  1. Use a version.py file only with __version__ = <VERSION> param in the file. In the setup.py file import the __version__ param and put it's value in the setup.py file like this: version=__version__
  2. Another way is to use just a setup.py file with version=<CURRENT_VERSION> - the CURRENT_VERSION is hardcoded.

Since we don't want to manually change the version in the file every time we create a new tag (ready to release a new package version), we can use the following..

I highly recommend bumpversion package. I've been using it for years to bump a version.

start by adding version=<VERSION> to your setup.py file if you don't have it already.

You should use a short script like this every time you bump a version:

bumpversion (patch|minor|major) - choose only one option
git push
git push --tags

Then add one file per repo called: .bumpversion.cfg:

[bumpversion]
current_version = <CURRENT_TAG>
commit = True
tag = True
tag_name = {new_version}
[bumpversion:file:<RELATIVE_PATH_TO_SETUP_FILE>]

Note:

  • You can use __version__ parameter under version.py file like it was suggested in other posts and update the bumpversion file like this: [bumpversion:file:<RELATIVE_PATH_TO_VERSION_FILE>]
  • You must git commit or git reset everything in your repo, otherwise you'll get a dirty repo error.
  • Make sure that your virtual environment includes the package of bumpversion, without it it will not work.

For what it's worth, if you're using NumPy distutils, numpy.distutils.misc_util.Configuration has a make_svn_version_py() method that embeds the revision number inside package.__svn_version__ in the variable version .

Related