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?

  1. I could drop the use of TypedDict and line-by-line suppress mypy

    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?

  2. 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 I 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
    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.