Subtleties of Python compatible release version specifier

Python’s most popular package management system - pip - comes to great help to developers and IMHO is significant part of Python’s popularity.

Popular way of defining project’s dependencies is using requirements.txt file. For example:

Jinja2
PyYAML
inflect
six

But given solution has a serious flow - it does not define which version of package to use. And there are strong arguments against this approach. Briefly, in the long run each dependency is going to have backward incompatible release.

Python has monstrous PEP-0440 that describes how to compose package version. One thing that not widely known is that version scheme is much more complex than 3 integers divided by dots. Here is format definition:

    [N!]N(.N)*[{a|b|rc}N][.postN][.devN][+<local version label>]

This allows crazy stuff like 3!0.10.1.3.112314rc100500.post7dev42+deadbeef

For end products it the most reliable solution is to pinpoint exact version using == operator, so that requirements.txt becomes:

Jinja2==2.8
PyYAML==3.11
inflect==0.2.5
six==1.10.0

Here I want to advertise pip-tools which helps to virtually hard-pin latest available versions.

Of course you don’t want to do this for libraries. Pip supports nice version operator called “compatible release” and it is written as ~=. It is tempting to think about as nearly equal. But it’s not. Imagine django extension, which is known to work only with django version 1.7. The obvious thing would be to write django ~= 1.7. WRONG. This expression is equivalent to django >= 1.7, == 1.*, that matches every version up to major release of django 2.0. Instead one should write django ~= 1.7.0 which converts to django >= 1.7.0, == 1.7.*.