423

I am walking a directory that contains eggs to add those eggs to the sys.path. If there are two versions of the same .egg in the directory, I want to add only the latest one.

I have a regular expression r"^(?P<eggName>\w+)-(?P<eggVersion>[\d\.]+)-.+\.egg$ to extract the name and version from the filename. The problem is comparing the version number, which is a string like 2.3.1.

Since I'm comparing strings, 2 sorts above 10, but that's not correct for versions.

>>> "2.3.1" > "10.1.1"
True

I could do some splitting, parsing, casting to int, etc., and I would eventually get a workaround. But this is Python, not Java. Is there an elegant way to compare version strings?

Stevoisiak
  • 23,794
  • 27
  • 122
  • 225
Savir
  • 17,568
  • 15
  • 82
  • 136

16 Answers16

665

Use packaging.version.parse.

>>> # pip install packaging
>>> from packaging import version
>>> version.parse("2.3.1") < version.parse("10.1.2")
True
>>> version.parse("1.3.a4") < version.parse("10.1.2")
True
>>> isinstance(version.parse("1.3.a4"), version.Version)
True
>>> isinstance(version.parse("1.3.xy123"), version.LegacyVersion)
True
>>> version.Version("1.3.xy123")
Traceback (most recent call last):
...
packaging.version.InvalidVersion: Invalid version: '1.3.xy123'

packaging.version.parse is a third-party utility but is used by setuptools (so you probably already have it installed) and is conformant to the current PEP 440; it will return a packaging.version.Version if the version is compliant and a packaging.version.LegacyVersion if not. The latter will always sort before valid versions.

Note: packaging has recently been vendored into setuptools.


An ancient and now deprecated method you might encounter is distutils.version, it's undocumented and conforms only to the superseded PEP 386;

>>> from distutils.version import LooseVersion, StrictVersion
>>> LooseVersion("2.3.1") < LooseVersion("10.1.2")
True
>>> StrictVersion("2.3.1") < StrictVersion("10.1.2")
True
>>> StrictVersion("1.3.a4")
Traceback (most recent call last):
...
ValueError: invalid version number '1.3.a4'

As you can see it sees valid PEP 440 versions as “not strict” and therefore doesn’t match modern Python’s notion of what a valid version is.

As distutils.version is undocumented, here are the relevant docstrings.

yEmreAk.com
  • 3,278
  • 2
  • 18
  • 37
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • 2
    Looks like NormalizedVersion will not be coming, as it was superseded, and LooseVersion and StrictVersion are therefore no longer deprecated. – Taywee Jun 17 '16 at 16:07
  • 21
    It's a crying shame `distutils.version` is undocumented. – John Y Aug 07 '17 at 19:07
  • found it using search engine, and finding directly the `version.py` source code. Very nicely put! – Joël Aug 24 '17 at 15:12
  • @Taywee they better are, since they’re not PEP 440 compliant. – flying sheep Feb 20 '19 at 14:34
  • Mind that the implementation of LooseVersion can lead you to really weird exceptions if you pass to the constructor anything that evaluates to False. In particular, LooseVersion(None) and LooseVersion('') are accepted silently at construction time, only to raise obscure exceptions when you later want to compare them to another because the __init__ method did exactly nothing. – agomcas Apr 08 '19 at 14:44
  • I get `AttributeError: module 'packaging' has no attribute 'version'` when I try to use the first method you mention. Using Python 3.7.3 – Josh Correia Jul 29 '19 at 13:59
  • 3
    imho `packaging.version.parse` can't be trusted to compare versions. Try `parse('1.0.1-beta.1') > parse('1.0.0')` for instance. – Trondh Aug 16 '19 at 08:57
  • @Trondh: I get `True`, which is what I was expecting. What do you get, and what were you expecting? – Mark Dickinson Aug 22 '19 at 16:58
  • @Trondh I added an answer that I believe resolves that issue. –  Aug 23 '19 at 23:41
  • Thanks! Yup, my testing was against semver and not pep440 which is why I got wonky results. – Trondh Sep 01 '19 at 08:05
  • To resolve error `AttributeError: module 'packaging' has no attribute 'version'` use this import statement instead `from packaging.version import Version, parse` – tyleax Jan 20 '20 at 19:53
  • Note that in python 3 comparing distuilts.version.LooseVersion objects will sometimes produce a typeerror. – plugwash Apr 29 '20 at 20:52
  • 12
    In Python 3.6+: `from pkg_resources import packaging` then `packaging.version.parse("0.1.1rc1") < packaging.version.parse("0.1.1rc2")` – gwelter Nov 07 '20 at 22:01
  • 1
    This is pretty buggy! `version.parse('0.34~') < version.parse('0.33')` returns True – Federico Dec 02 '20 at 20:42
  • Super useful that you linked to the LooseVersion code! Since it's undocumented, the docstrings were exactly what I needed to realize why I was having a bug (and convince me to upgrade!)... specifically that it doesn't support versions with 4 sections like 1.2.3.4 – Sean Colombo Feb 25 '21 at 14:56
133

The packaging library contains utilities for working with versions and other packaging-related functionality. This implements PEP 0440 -- Version Identification and is also able to parse versions that don't follow the PEP. It is used by pip, and other common Python tools to provide version parsing and comparison.

$ pip install packaging
from packaging.version import parse as parse_version
version = parse_version('1.0.3.dev')

This was split off from the original code in setuptools and pkg_resources to provide a more lightweight and faster package.


Before the packaging library existed, this functionality was (and can still be) found in pkg_resources, a package provided by setuptools. However, this is no longer preferred as setuptools is no longer guaranteed to be installed (other packaging tools exist), and pkg_resources ironically uses quite a lot of resources when imported. However, all the docs and discussion are still relevant.

From the parse_version() docs:

Parsed a project's version string as defined by PEP 440. The returned value will be an object that represents the version. These objects may be compared to each other and sorted. The sorting algorithm is as defined by PEP 440 with the addition that any version which is not a valid PEP 440 version will be considered less than any valid PEP 440 version and the invalid versions will continue sorting using the original algorithm.

The "original algorithm" referenced was defined in older versions of the docs, before PEP 440 existed.

Semantically, the format is a rough cross between distutils' StrictVersion and LooseVersion classes; if you give it versions that would work with StrictVersion, then they will compare the same way. Otherwise, comparisons are more like a "smarter" form of LooseVersion. It is possible to create pathological version coding schemes that will fool this parser, but they should be very rare in practice.

The documentation provides some examples:

If you want to be certain that your chosen numbering scheme works the way you think it will, you can use the pkg_resources.parse_version() function to compare different version numbers:

>>> from pkg_resources import parse_version
>>> parse_version('1.9.a.dev') == parse_version('1.9a0dev')
True
>>> parse_version('2.1-rc2') < parse_version('2.1')
True
>>> parse_version('0.6a9dev-r41475') < parse_version('0.6a9')
True
Joris Timmermans
  • 10,814
  • 2
  • 49
  • 75
davidism
  • 121,510
  • 29
  • 395
  • 339
85
def versiontuple(v):
    return tuple(map(int, (v.split("."))))

>>> versiontuple("2.3.1") > versiontuple("10.1.1")
False
kindall
  • 178,883
  • 35
  • 278
  • 309
  • 11
    The other answers are in the standard library and follow PEP standards. – Chris Aug 15 '14 at 18:56
  • Thanks for this! By changing the 'int' type in the map function to 'str' this can also handily accomodate both numeric and alpha-numeric version numbers. `def versiontuple(v): return tuple(map(str, (v.split("."))))` – Phaxmohdem Feb 13 '15 at 17:02
  • 1
    In that case you could remove the `map()` function entirely, as the result of `split()` is *already* strings. But you don't want to do that anyway, because the whole reason to change them to `int` is so that they compare properly as numbers. Otherwise `"10" < "2"`. – kindall Feb 13 '15 at 17:56
  • Good point. I was able to work around this by padding each item in the tuple with leading zeros using the `zfill()` function. – Phaxmohdem Feb 17 '15 at 17:46
  • 10
    This will fail for something like `versiontuple("1.0") > versiontuple("1")`. The versions are the same, but the tuples created `(1,)!=(1,0)` – dawg May 18 '15 at 18:49
  • 7
    In what sense are version 1 and version 1.0 the same? Version numbers aren't floats. – kindall Jul 17 '15 at 17:11
  • 20
    **No, this should _not_ be the accepted answer.** Thankfully, it isn't. Reliable parsing of version specifiers is non-trivial (if not practically infeasible) in the general case. Don't reinvent the wheel and then proceed to break it. As [ecatmur](https://stackoverflow.com/users/567292/ecatmur) suggests [above](https://stackoverflow.com/a/11887885/2809027), just use `distutils.version.LooseVersion`. That's what it's there for. – Cecil Curry Mar 01 '16 at 00:56
  • 5
    @chris when packaging an application the other answers require you to add all of distutils or all of packaging and pkg_resources ... which are a bit of bloat. this is a useful answer that works much of the time - and doesn't lead to package bloat. it really depends on the context. – Erik Aronesty Sep 30 '19 at 15:12
  • Despite its limitations, I have chosen this approach because I think that the version codes of the tool I want to compare versions (Git) will behave reliably, and I do not want to rely on a pip library (which may not be present in my target environment) or a deprecated library. Just to play safe, I decided to only use the first two numbers of Git's version code and disregard everything else. – Akira Cleber Nakandakare Mar 02 '23 at 16:01
33

What's wrong with transforming the version string into a tuple and going from there? Seems elegant enough for me

>>> (2,3,1) < (10,1,1)
True
>>> (2,3,1) < (10,1,1,1)
True
>>> (2,3,1,10) < (10,1,1,1)
True
>>> (10,3,1,10) < (10,1,1,1)
False
>>> (10,3,1,10) < (10,4,1,1)
True

@kindall's solution is a quick example of how good the code would look.

Gabi Purcaru
  • 30,940
  • 9
  • 79
  • 95
  • 4
    I think this answer could be expanded upon by providing code that performs the transformation of a __PEP440__ string into a tuple. I think you will find it's not a trivial task. I think it's better left to the package that performs that translation for `setuptools`, which is `pkg_resources`. –  Aug 24 '19 at 00:38
  • 1
    @TylerGubala this is a great answer in situations where you know that the version is and always will be "simple". pkg_resources is a big package and can cause a distributed executable to be rather bloated. – Erik Aronesty Sep 30 '19 at 15:15
  • @Erik Aronesty I think version control inside of distributed executables is somewhat ouside of the scope of the question, but I agree, generally at least. I think though that there is something to be said about the reusability of `pkg_resources`, and that assumptions of simple package naming may not always be ideal. –  Sep 30 '19 at 15:46
  • 3
    It works great for making sure `sys.version_info > (3, 6)` or whatever. – Gqqnbig Feb 04 '20 at 19:20
20

The way that setuptools does it, it uses the pkg_resources.parse_version function. It should be PEP440 compliant.

Example:

#! /usr/bin/python
# -*- coding: utf-8 -*-
"""Example comparing two PEP440 formatted versions
"""
import pkg_resources

VERSION_A = pkg_resources.parse_version("1.0.1-beta.1")
VERSION_B = pkg_resources.parse_version("v2.67-rc")
VERSION_C = pkg_resources.parse_version("2.67rc")
VERSION_D = pkg_resources.parse_version("2.67rc1")
VERSION_E = pkg_resources.parse_version("1.0.0")

print(VERSION_A)
print(VERSION_B)
print(VERSION_C)
print(VERSION_D)

print(VERSION_A==VERSION_B) #FALSE
print(VERSION_B==VERSION_C) #TRUE
print(VERSION_C==VERSION_D) #FALSE
print(VERSION_A==VERSION_E) #FALSE
  • `pkg_resources` is part of `setuptools`, which depends on `packaging`. See other answers that discuss `packaging.version.parse`, which has an identical implementation to `pkg_resources.parse_version`. – Jed Dec 11 '19 at 21:32
  • Moreover, it now uses packaging as vendor. – Andrei Aug 05 '20 at 12:58
  • 6
    @Jed I don't think `setuptools` depends on `packaging`. I can import `setuptools` and `pkg_resources`, but `import packaging` raise ImportError. – Conchylicultor Sep 24 '20 at 09:42
  • 1
    this is the only solution that worked in 16.04.6 LTS, python3.8 – rok Nov 22 '20 at 11:27
13

There is packaging package available, which will allow you to compare versions as per PEP-440, as well as legacy versions.

>>> from packaging.version import Version, LegacyVersion
>>> Version('1.1') < Version('1.2')
True
>>> Version('1.2.dev4+deadbeef') < Version('1.2')
True
>>> Version('1.2.8.5') <= Version('1.2')
False
>>> Version('1.2.8.5') <= Version('1.2.8.6')
True

Legacy version support:

>>> LegacyVersion('1.2.8.5-5-gdeadbeef')
<LegacyVersion('1.2.8.5-5-gdeadbeef')>

Comparing legacy version with PEP-440 version.

>>> LegacyVersion('1.2.8.5-5-gdeadbeef') < Version('1.2.8.6')
True
sashk
  • 3,946
  • 2
  • 33
  • 41
  • 4
    For those wondering about the difference between `packaging.version.Version` and `packaging.version.parse`: "[`version.parse`] takes a version string and will parse it as a `Version` if the version is a valid PEP 440 version, otherwise it will parse it as a `LegacyVersion`." (whereas `version.Version` would raise `InvalidVersion`; [source](https://packaging.pypa.io/en/latest/version/#packaging.version.parse)) – Braham Snyder Mar 02 '18 at 16:05
  • NB: `LooseVersion` yields a deprecation warning in 3.10: `DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 6s` – maxschlepzig Nov 06 '21 at 16:07
11

Posting my full function based on Kindall's solution. I was able to support any alphanumeric characters mixed in with the numbers by padding each version section with leading zeros.

While certainly not as pretty as his one-liner function, it seems to work well with alpha-numeric version numbers. (Just be sure to set the zfill(#) value appropriately if you have long strings in your versioning system.)

def versiontuple(v):
   filled = []
   for point in v.split("."):
      filled.append(point.zfill(8))
   return tuple(filled)

.

>>> versiontuple("10a.4.5.23-alpha") > versiontuple("2a.4.5.23-alpha")
True


>>> "10a.4.5.23-alpha" > "2a.4.5.23-alpha"
False
Phaxmohdem
  • 521
  • 5
  • 6
  • Possible improvements: 1) split string by a (compiled) `[.+-]` regex (and not just `.`) 2) determine the maximum substring length and use that for zfill - see also [my gist](https://gist.github.com/gsauthof/9a00f5fb7b45ac45fcc93b7582c7590b) – maxschlepzig Jul 14 '22 at 14:39
9

You can use the semver package to determine if a version satisfies a semantic version requirement. This is not the same as comparing two actual versions, but is a type of comparison.

For example, version 3.6.0+1234 should be the same as 3.6.0.

import semver
semver.match('3.6.0+1234', '==3.6.0')
# True

from packaging import version
version.parse('3.6.0+1234') == version.parse('3.6.0')
# False

from distutils.version import LooseVersion
LooseVersion('3.6.0+1234') == LooseVersion('3.6.0')
# False
davidism
  • 121,510
  • 29
  • 395
  • 339
Prikkeldraad
  • 1,347
  • 9
  • 14
1

I was looking for a solution which wouldn't add any new dependencies. Check out the following (Python 3) solution:

class VersionManager:

    @staticmethod
    def compare_version_tuples(
            major_a, minor_a, bugfix_a,
            major_b, minor_b, bugfix_b,
    ):

        """
        Compare two versions a and b, each consisting of 3 integers
        (compare these as tuples)

        version_a: major_a, minor_a, bugfix_a
        version_b: major_b, minor_b, bugfix_b

        :param major_a: first part of a
        :param minor_a: second part of a
        :param bugfix_a: third part of a

        :param major_b: first part of b
        :param minor_b: second part of b
        :param bugfix_b: third part of b

        :return:    1 if a  > b
                    0 if a == b
                   -1 if a  < b
        """
        tuple_a = major_a, minor_a, bugfix_a
        tuple_b = major_b, minor_b, bugfix_b
        if tuple_a > tuple_b:
            return 1
        if tuple_b > tuple_a:
            return -1
        return 0

    @staticmethod
    def compare_version_integers(
            major_a, minor_a, bugfix_a,
            major_b, minor_b, bugfix_b,
    ):
        """
        Compare two versions a and b, each consisting of 3 integers
        (compare these as integers)

        version_a: major_a, minor_a, bugfix_a
        version_b: major_b, minor_b, bugfix_b

        :param major_a: first part of a
        :param minor_a: second part of a
        :param bugfix_a: third part of a

        :param major_b: first part of b
        :param minor_b: second part of b
        :param bugfix_b: third part of b

        :return:    1 if a  > b
                    0 if a == b
                   -1 if a  < b
        """
        # --
        if major_a > major_b:
            return 1
        if major_b > major_a:
            return -1
        # --
        if minor_a > minor_b:
            return 1
        if minor_b > minor_a:
            return -1
        # --
        if bugfix_a > bugfix_b:
            return 1
        if bugfix_b > bugfix_a:
            return -1
        # --
        return 0

    @staticmethod
    def test_compare_versions():
        functions = [
            (VersionManager.compare_version_tuples, "VersionManager.compare_version_tuples"),
            (VersionManager.compare_version_integers, "VersionManager.compare_version_integers"),
        ]
        data = [
            # expected result, version a, version b
            (1, 1, 0, 0, 0, 0, 1),
            (1, 1, 5, 5, 0, 5, 5),
            (1, 1, 0, 5, 0, 0, 5),
            (1, 0, 2, 0, 0, 1, 1),
            (1, 2, 0, 0, 1, 1, 0),
            (0, 0, 0, 0, 0, 0, 0),
            (0, -1, -1, -1, -1, -1, -1),  # works even with negative version numbers :)
            (0, 2, 2, 2, 2, 2, 2),
            (-1, 5, 5, 0, 6, 5, 0),
            (-1, 5, 5, 0, 5, 9, 0),
            (-1, 5, 5, 5, 5, 5, 6),
            (-1, 2, 5, 7, 2, 5, 8),
        ]
        count = len(data)
        index = 1
        for expected_result, major_a, minor_a, bugfix_a, major_b, minor_b, bugfix_b in data:
            for function_callback, function_name in functions:
                actual_result = function_callback(
                    major_a=major_a, minor_a=minor_a, bugfix_a=bugfix_a,
                    major_b=major_b, minor_b=minor_b, bugfix_b=bugfix_b,
                )
                outcome = expected_result == actual_result
                message = "{}/{}: {}: {}: a={}.{}.{} b={}.{}.{} expected={} actual={}".format(
                    index, count,
                    "ok" if outcome is True else "fail",
                    function_name,
                    major_a, minor_a, bugfix_a,
                    major_b, minor_b, bugfix_b,
                    expected_result, actual_result
                )
                print(message)
                assert outcome is True
                index += 1
        # test passed!


if __name__ == '__main__':
    VersionManager.test_compare_versions()

EDIT: added variant with tuple comparison. Of course the variant with tuple comparison is nicer, but I was looking for the variant with integer comparison

Stefan Saru
  • 1,731
  • 2
  • 10
  • 4
  • I am curious in what situation does this avoid adding dependencies? Won't you need the packaging library (used by setuptools) to create a python package? – Josiah L. Apr 25 '20 at 06:20
  • @JosiahL. Avoiding such a dependency makes sense when you are using your code on hosts where you don't package anything (think: production server vs. developer workstation). However, this code doesn't answer the question, because it assumes that you already have decomposed/converted your version string by yourself - while the question is about comparing version strings such as `"2.3.1" > "10.1.1"`. Also, I don't see the point of wrapping this as a static method in a class. – maxschlepzig Jul 14 '22 at 14:51
0

... and getting back to easy ... for simple scripts you can use:

import sys
needs = (3, 9) # or whatever
pvi = sys.version_info.major, sys.version_info.minor    

later in your code

try:
    assert pvi >= needs
except:
    print("will fail!")
    # etc.
nairoby
  • 55
  • 4
0

To increment version using python

def increment_version(version):
    version = version.split('.')
    if int(version[len(version) - 1]) >= 99:
        version[len(version) - 1] = '0'
        version[len(version) - 2] = str(int(version[len(version) - 2]) + 1)
    else:
        version[len(version) - 1] = str(int(version[len(version) - 1]) + 1)
    return '.'.join(version)

version = "1.0.0"
version_type_2 = "1.0"
print("old version",version ,"new version",increment_version(version))
print("old version",version_type_2 ,"new version",increment_version(version_type_2))
Abanoub Hany
  • 557
  • 4
  • 7
0

This is a compact code for comparing three version numbers. Note that the string comparison fails for all pairs here.

from itertools import permutations

for v1, v2 in permutations(["3.10.21", "3.10.3", "3.9.9"], 2):
    print(f"\nv1 = {v1}, v2 = {v2}")
    print(f"v1 < v2      version.parse(v1) < version.parse(v2)")
    print(f"{v1 < v2}         {version.parse(v1) < version.parse(v2)}")

That gives us:

v1='3.10.21', v2='3.10.3'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.10.21', v2='3.9.9'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.10.3', v2='3.10.21'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

v1='3.10.3', v2='3.9.9'
v1 < v2      version.parse(v1) < version.parse(v2)
True         False

v1='3.9.9', v2='3.10.21'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

v1='3.9.9', v2='3.10.3'
v1 < v2      version.parse(v1) < version.parse(v2)
False         True

permutations(iterable, 2) gives us all the 2-length permutations of an iterable. So for example

list(permutations('ABC', 2))

gives us [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')].

-1

similar to standard strverscmp and similar to this solution by Mark Byers but using findall instead of split to avoid empty case.

import re
num_split_re = re.compile(r'([0-9]+|[^0-9]+)')

def try_int(i, fallback=None):
    try:
        return int(i)
    except ValueError:
        pass
    except TypeError:
        pass
    return fallback

def ver_as_list(a):
    return [try_int(i, i) for i in num_split_re.findall(a)]

def strverscmp_lt(a, b):
    a_ls = ver_as_list(a)
    b_ls = ver_as_list(b)
    return a_ls < b_ls
Muayyad Alsadi
  • 1,506
  • 15
  • 23
-1

Here is something that will work assuming your semantic versions are "clean" (e.g. x.x.x) and you have a list of versions you need to sort.

# Here are some versions
versions = ["1.0.0", "1.10.0", "1.9.0"]

# This does not work
versions.sort() # Result: ['1.0.0', '1.10.0', '1.9.0']

# So make a list of tuple versions
tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]

# And sort the string list based on the tuple list
versions = [x for _, x in sorted(zip(tuple_versions, versions))] # Result: ['1.0.0', '1.9.0', '1.10.0']

To get the latest version you could just select the last element in the list versions[-1] or reverse sort by using the reverse attribute of sorted(), setting it to True, and getting the [0] element.

You could of course then wrap all this up in a convenient function for reuse.

def get_latest_version(versions):
    """
    Get the latest version from a list of versions.
    """
    try:
        tuple_versions = [tuple(map(int, (version.split(".")))) for version in versions]
        versions = [x for _, x in sorted(zip(tuple_versions, versions), reverse=True)]
        latest_version = versions[0]
    except Exception as e:
        print(e)
        latest_version = None

    return latest_version

print(get_latest_version(["1.0.0", "1.10.0", "1.9.0"]))
Justin
  • 707
  • 7
  • 13
  • How is this an improvement over [kindall's answer](https://stackoverflow.com/a/11887825/427158) (posted in 2012) when comparing two version strings? It seems like you are trying to answer a different question. – maxschlepzig Jul 14 '22 at 14:56
-1

If you want to create a filter on a library version, you may use the __version__ attribute (here an example with the jwt library):

from packaging import version
import jwt

if version.parse(jwt.__version__) < version.parse('2.0.0'):
    # TODO: your code

Galuoises
  • 2,630
  • 24
  • 30
-1

simple few-liner:

import sys
if (sys.version_info.major, sys.version_info.minor) >= (3, 9):
    ...
else:
    ...
Dan M
  • 1,175
  • 12
  • 23