Feature Checking versus Version Checking

Bruno Oliveira, known for his work on the pytest project, tweeted this thread last July:
I always prefer explicitly checking versions instead of checking for presence of features:
# feature checking try: from importlib import metadata except ImportError: import importlib_metadata as metadata # explicit checking if sys.version_info >= (3, 8): from importlib import metadata else: import importlib_metadata as metadataReasons:
1) This makes it explicit which versions supports the desired functionality, which serves as documentation and makes it easy to drop the legacy code later when that version is no longer supported: search for sys.version_info in the codebase and remove the old code.
2) In the specific case of ImportErrors, broken environments might cause an otherwise valid import to raise an error, which would then hide the real reason why the import failed. Had seen this myself a few times in "frozen" applications (cx_freeze, pyinstaller) where build problems caused ImportErrors which should not happen on that version.
I have also moved towards doing this myself, for the same reasons.
1. It’s More Explicit
Being more explicit, is the primary appeal to me. Feature-checking is normally accompanied with an explanatory comment about version numbers anyway. For example here’s some code I had in an old version of Django-MySQL:
try:
# Django 1.11+
from django.utils.text import format_lazy
def lazy_string_concat(*strings):
return format_lazy("{}" * len(strings), *strings)
except ImportError:
from django.utils.translation import string_concat as lazy_string_concat
That # Django 1.11
comment contains useful information outside of code. It could also, like any comment, be a lie.
Additionally, unless you are very disciplined in how you format such comments, it can be hard to find them all during upgrades.
With version checking, the comment becomes code:
if django.VERSION >= (1, 11):
from django.utils.text import format_lazy
def lazy_string_concat(*strings):
return format_lazy("{}" * len(strings), *strings)
else:
from django.utils.translation import string_concat as lazy_string_concat
2. It Reveals Broken Sub-Imports
The second reason, unexpected sub-import failures, is a case of bug silencing.
I’ve not worked with frozen environments as Bruno has, but it can happen in other situations. For example, importing a Python module can fail if it tries to import a missing C library. Such ImportError
s are not related to the feature check, but the other code path would be mistakenly run as well.
This 2011 post from Armin Ronacher covers the problem in depth. It’s a good read and has a workaround “cautious import” function, which avoids catching sub-module ImportError
s.
Fin
I hope this helps you build better Python projects that support multiple language or library versions.
—Adam
If your Django project’s long test runs bore you, I wrote a book that can help.
One summary email a week, no spam, I pinky promise.
Related posts: