Python Type Hints - How to Handle Optional Imports

Import, or import not, there is no try.

This post is not about importing typing.Optional, but instead imports that are themselves optional. Libraries often have optional dependencies, and the code should work whether or not the import is there. A common pattern to solve this to catch ImportError and replace the module with None:

try:
    import markdown
except ImportError:
    markdown = None

Later, code that may use the module checks if the name is not None:

def do_something():
    ...

    if markdown is not None:
        ...

This pattern works fine—Python has no qualms. But, Mypy does. If you run Mypy on the above try block, it will report:

$ mypy example.py
example.py:4: error: Incompatible types in assignment (expression has type "None", variable has type Module)

Oh gosh! Mypy sees the import statement and infers that markdown has the type ModuleType only. It therefore doesn’t allow assignment of None to such a variable.

Potentially, a future version of Mypy could detect this pattern, and instead infer that markdown has type ModuleType | None. Version 0.920 included a similar change, to allow a None assignment in an else block to affect a variable’s inferred type. (See “Making a Variable Optional in an Else Block” in the release post.) But, at least at time of writing, you have to use a different pattern.

The solution I’ve found is to use a second bool variable to indicate whether the module imported:

try:
    import markdown

    HAVE_MARKDOWN = True
except ImportError:
    HAVE_MARKDOWN = False


def something():
    ...

    if HAVE_MARKDOWN:
        ...

Mypy is just fine with this:

$ mypy example.py
Success: no issues found in 1 source file

Fantastic!

Well, that’s the trick. Go make your optional imports work.

A Non-Functioning Alternative

Some readers may have wondered if it’s possible to pre-declare the type of markdown before its import:

from types import ModuleType

markdown: ModuleType | None

try:
    import markdown
except ImportError:
    markdown = None

Unfortunately, Mypy does not allow import to replace a pre-declared variable:

$ mypy example.py
example.py:6: error: Name "markdown" already defined on line 3

Aw, shucks. It seems Mypy’s interpretation of import is pretty strict. It probably is strict with good reason, as it uses import statements to fetch related type hints.

Fin

May your optional imports pass all type checks,

—Adam


Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

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

Related posts:

Tags: ,