Until recently I had been able to specify a Python version and work with it. Here’s a brief outline of some work I did recently to support a language feature that changed across a few Python versions.
Fortunately I’m not having to support Python2 versus Python3 features. Until recently the language features that I’ve used have been uniform across the major 3.x Python versions (3.6, 3.7 and 3.8).
So for a little context: my development system is running Python 3.8 by default. I’m “in the flow”, working away and I get some mypy errors complaining about undeclared dictionary types so I quickly implement use of typing.TypedDict
. In accordance with PEP-589, static analysis tools such as mypy will report type violations based on type annotations and thus you can use typing.TypedDict
to satisfy mypy regarding dictionary type structure. Useful.
But then I push to CI which checks all the tests across multiple Python versions and TypedDict
is not implemented in Python 3.6 or 3.7 for which I have to maintain language compatibility. So what to do?
-
I could drop the use of
TypedDict
and line-by-line suppress mypy
errors…The problem is that I would still like to have some kind of dictionary validation which probably means using something like schema which I’ve used before and I like, so maybe it’s all good?
-
Or maybe there’s a way to back-port language features from 3.8 to the earlier versions?
It turns out there’s a way to do that…
Observation #1
There’s an official package called typing-extensions for back-porting selected typing
features to older Python releases.
But if I include that in my project dependencies, doesn’t that mean that Python 3.8 would have this extra baggage of an unnecessary package that conceivably might also accidentally cause conflicts, or even bugs?…
Observation #2
PEP-508 specifies a means for describing Python release specific project dependencies which is also supported by my favourite package manager flit.
So my pyproject.toml
just needs to include something like this:
# pyproject.toml
requires = [
# Python 3.6, 3.7 support for Python 3.8 typing features:
"typing-extensions; python_version <'3.8'",
]
Now the typing-extensions package is only included for Python releases earlier than 3.8. Nice.
Observation #3
You can conditionally import packages. Using the standard sys.version_info
data you can condition the import to import different packages depending on the currently running Python version.
# my_project/typing.py
import sys
if ((sys.version_info[0] == 3) and (sys.version_info[1] >= 8)) or (
sys.version_info[0] >= 4
):
from typing import TypedDict
elif (sys.version_info[0] == 3) and (sys.version_info[1] in [6, 7]):
from typing_extensions import TypedDict
else:
raise RuntimeError("Unsupported Python version, {0}".format(sys.version_info))
I’m only interested in the TypedDict
feature at present, but hopefully it’s obvious that you could import any of the other features in typing_extensions if you needed them.
By adding a new typing.py
module in my project that localises the conditional import to a single location I can continue to create maintainable, uniform code that will use the TypedDict
language feature.
# my_project/some_module.py
from my_project.typing import TypedDict
def my_function(some_value: TypedDict)->None:
raise NotImplementedError()
And we’re done
Hopefully this was an informative description of one way to cleanly support newer language features across multiple Python versions.