It is useful to know the version of the underlying library wrapped in the Python module. This allows to test what features are supported, what bugs should be worked around, because not all features can be tested by hasattr() (especially if the implementation was buggy in some versions). There are two versions – the library version that was used for building the module, and the library version actually loaded. Both are important, the wrapper cannot use features that was not available at compile time, and the behavior depends on the runtime version. There are also two forms – as a machine friendly tuple or named tuple and as a human readable string (it can contain details not exposed in the tuple form). So we can have up to four variable (some libraries can provide only compile-time or runtime version):
compile-time version as a string
compile-time version as a tuple or a named tuple
runtime version as a string
runtime version as a tuple or a named tuple
Existing variable:
sys.version – compile-/runtime version as a string
sys.version_info – compile-/runtime version as a named tuple
sys.getwindowsversion() – runtime version as a named tuple
curses.ncurses_version – runtime (falls back to compile-time) version as a named tuple
sqlite3.sqlite_version – runtime version as a string
sqlite3.sqlite_version_info – runtime version as a named tuple
zlib.ZLIB_VERSION – compile-time version as a string
zlib.ZLIB_RUNTIME_VERSION – runtime version as a string
zlib.ZLIBNG_VERSION – compile-time version as a string
zstd.zstd_version – runtime version as a string
zstd.zstd_version_info – runtime version as a tuple
pyexpat.EXPAT_VERSION – runtime version as a string
pyexpat.version_info – runtime version as a tuple
decimal.SPEC_VERSION – compile-time version as a string
decimal.__libmpdec_version__ – runtime version as a string (not the same as SPEC_VERSION)
tkinter.TclVersion/tkinter.TkVersion – compile-time version as a float (ambiguous!)
_tkinter.TCL_VERSION/_tkinter.TK_VERSION – compile-time version as a string (not public)
tkinter.BaseWidget.info_patchlevel() – runtime version as a named tuple and a string
As you see, while there are some patterns, they are not consistent. It would be nice to have consistant pattern, so you always can tell the meaning by the name, without looking in the documentation.
Should the library name be included in the name? For curses we have multiple implementations, this is necessary. For zlib, we support an alternative library zlib-ng, this is also needed.
Should we use upper case for string and lower case for tuple? Or _version for string and _version_tuple for tuple? Or add _string for string?
Should we combine a named tuple and a string in the same variable, like in Tkinter’s pathinfo() (the result is a named tuple, but it’s __str__() returns the string version)? This will complicate implementation, because not always the string version can be derived from a tuple.
Should we use the “runtime” infix for runtime version and no infix for compile-time version or the “header” infix for compile-time version and no infix for runtime version? Or use infixes for both versions?
Old names can be left as aliases, but we should avoid changing the meaning of old names.
We’ve typically avoided having multiple names with aliases for the same thing. It leads to confusion (are these two things the same?) and complicates automated tooling. Are you proposing a period after which the old “bad” names would be removed?
Mostly, why are these variables important enough to churn with aliases? In my experience, code using specialized variables like this have much deeper and subtler things to keep in mind than the names of the variables, so documentation will always be consulted anyway.
I’ll reply with my old red hat on[1] – err, for a generic third party that provides long-term support for open-source libraries:
Fixes can be selectively backported to older versions, with the version either unchanged or given some tag that you generally can’t account for. If there’s any other way to check for a feature/fix, you should do that rather than check the version.
-1 to making this easier.
I left the company but kept the souvenir headgear ↩︎
I always end up just testing for features with try/except when I need to. Setting some global flag somewhere that can be tested against. It’s always more reliable to actually know that something works than assume it works because of a version number.
If the names are not consistent, we inevitably will get requests to fix them, until we give up and add aliases. Then we will get recurrent cleanup requests. This is unavoidably. We can resist this for a time by keeping aliases in “soft-deprecated” state without any termins of hard deprecation.
Mostly, why are these variables important enough to churn with aliases?
If names are inconsistent, you can use foo_version and think that it is a header version, while in fact it is a runtime version, or vice versa. This may be the same on the developer’s computer, but different on the user’s computers. It is important to have some conbvention, or several conventions if they are not conflicting.
Yes, we had issues here with Expat. But not all features can be easily tested at runtime. It may be not a new function or method, but a new parameter or new value accepted as argument. You may wond t chose one way or other depending on version, even if both ways are supported, because a particular way was optimized in new version. You may need to work around known bugs in old versions.
Also, you need versions for diagnostic.
Since several new versions were added after I started working on this issue, and they use _version and _version_info suffixes, I am going to follow that trend. The question is how to distinguish compile time from runtime vesions. Use ALLCAPS or _header_ for compile time versions? The former would preserve ZLIB_VERSION, but EXPAT_VERSION needs to be changed.