Feature Checking versus Version Checking

Measure ye these two approaches

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 metadata

Reasons:

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 ImportErrors 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 ImportErrors.

Fin

I hope this helps you build better Python projects that support multiple language or library versions.

—Adam


Read my book Boost Your Git DX to Git better.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: ,