Python Type Hints - How to Handle Optional Imports

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.
Learn how to make your tests run quickly in my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.
Related posts: