Janis Lesinskis' Blog

Assorted ramblings

  • All entries
  • About me
  • Projects
  • Economics
  • Misc
  • Software-engineering
  • Sports

Finding the versions of imported modules in Python code


A common question I run into is how to find the version of an imported module in Python at runtime. I get asked this very frequently when running python workshops. There are a few reasons that you may want to do this, for example you may be writing a library where you want to check if one of the modules you are using is a certain version so that you can decide which code to run. For example:

if library_major_version > 4:
    print("Using new feature")
else:
    print("Using older feature")

The tricky thing is being able to reliably find the version. The simplest thing is if the __version__ attribute exists on the library:

>>> import pandas
>>> pandas.__version__
'0.23.4'

As you can see a good practice is to define this __version__ attribute when you are writing your own libraries and modules. You may find tooling like bump2version to be useful for keeping your version info in sync.

Trying to access __version__ unfortunately won't work everywhere, most notably this is missing on some standard library modules where the assumption is that the version is assumed to be taken from the overall version of the Python installation itself. For example in Python 3.7.3:

$ python
Python 3.7.1 (default, Dec 14 2018, 13:28:58) 
[Clang 4.0.1 (tags/RELEASE_401/final)] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import string
>>> string.__version__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'string' has no attribute '__version__'

In this case you'd want to check against the version of Python directly:

>>> import sys
>>> sys.version
'3.7.1 (default, Dec 14 2018, 13:28:58) \n[Clang 4.0.1 (tags/RELEASE_401/final)]'
>>> sys.version_info
sys.version_info(major=3, minor=7, micro=1, releaselevel='final', serial=0)

While the format of the version numbers is fairly well known, see PEP 440 for details, the location at which this version can be accessed is not so clear. This is a common enough issue that PEP 396 was created in 2011 to discuss some potential resolutions to providing version information about the installed versions of modules.

This PEP hasn't seen much movement until Python 3.8, so what can you do on older versions.

What can you do when there's no __version__

When you install a package with something like pip the package will end up inß site-packages. Hopefully the 3rd party library authors have gone to the effort of specifying __version__ but what can we do if this attribute is not defined?

>>> import some_example_lib
>>> some_example_lib.__version__
>>> no_version_attribute.__version__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'some_example_lib' has no attribute '__version__'

If this is just a Python file that's in your system path you are out of luck, however if your file was installed using pip there's some metadata you can access. As you may have noticed when pip installs the library there's a piece of metadata that's required in the setup.py or similar that the library will have to provide containing the version number.

setup(
    name="some_example_lib",
    version="1.2.3",
    # more setup.py options here...
)

If a library was installed with a sufficiently modern version of setuptools you will have access to the package data via pkg_resources like so:

>>> import pkg_resources
>>> import some_example_lib
>>> pkg_resources.get_distribution("some_example_lib").version
"1.2.3"

The availability of this, amongst many other reasons, is why updating your setuptools is a good idea:

pip install -U setuptools

Why I didn't use package_name.__name__ to find the package Distribution

Some packages have a different package name and distribution name. A good example of this is beautifulsoup4, when we install we do:

pip install beautifulsoup4

But when we import it to use it we do:

import bs4

If we look at the setup.py for beautifulsoup we will see the following relevant sections of setup:

from setuptools import (
    setup,
    find_packages,
)
setup(
    name="beautifulsoup4",
    # NOTE: We can't import __version__ from bs4 because bs4/__init__.py is Python 2 code,
    # and converting it to Python 3 means going through this code to run 2to3.
    # So we have to specify it twice for the time being.
    version = '4.8.1',
    # more config
    packages=find_packages(exclude=['tests*']),
    # more config
)

When the setup for this is running the find_packages here will find a package called bs4 which is found in the project at ./bs4 and this is what we are able to eventually import via import bs4 in our code. Note that the names don't match here between the import and the package. We could in theory parse this from the setup.py here but that may not be easy at all. Take for example a library I've been using lately when I've been running training courses for Network and Systems engineers, ruamel.yaml

From the setup.py of that project we see that the name for the package is generated by a bunch of code.

pip install ruamel.yaml

When you end up running it you get something like this:

>>> import yaml
>>> yaml.__name__
'yaml'
>>> pkg_resources.get_distribution(yaml.__name__).version
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/janis/anaconda3/lib/python3.7/site-packages/pkg_resources/__init__.py", line 481, in get_distribution
    dist = get_provider(dist)
  File "/Users/janis/anaconda3/lib/python3.7/site-packages/pkg_resources/__init__.py", line 357, in get_provider
    return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
  File "/Users/janis/anaconda3/lib/python3.7/site-packages/pkg_resources/__init__.py", line 900, in require
    needed = self.resolve(parse_requirements(requirements))
  File "/Users/janis/anaconda3/lib/python3.7/site-packages/pkg_resources/__init__.py", line 786, in resolve
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'yaml' distribution was not found and is required by the application
>>> yaml.__package__
'yaml'
>>> pkg_resources.get_distribution(yaml.__package__).version
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/janis/anaconda3/lib/python3.7/site-packages/pkg_resources/__init__.py", line 481, in get_distribution
    dist = get_provider(dist)
  File "/Users/janis/anaconda3/lib/python3.7/site-packages/pkg_resources/__init__.py", line 357, in get_provider
    return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
  File "/Users/janis/anaconda3/lib/python3.7/site-packages/pkg_resources/__init__.py", line 900, in require
    needed = self.resolve(parse_requirements(requirements))
  File "/Users/janis/anaconda3/lib/python3.7/site-packages/pkg_resources/__init__.py", line 786, in resolve
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'yaml' distribution was not found and is required by the application

As you can see you can't rely upon the attributes __name__ and __version__ to work here since both those resolve to the string "yaml" but the package installation is actually found at "ruamel.yaml".

>>> pkg_resources.get_distribution("ruamel.yaml").version
'0.16.5'
Published: Thu 12 December 2019
By Janis Lesinskis
In Software-engineering
Tags: python package-management pip modules

links

  • Amazon wishlist

social

  • My GitHub page
  • LinkedIn

Proudly powered by Pelican, which takes great advantage of Python.