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'