Typing an unimported module

Question:

I have a Python package that has an optional [extras] dependency, yet I want to adhere to typing on all methods.

The situation is that in my file, I have this

class MyClass:

    def __init__(self, datastore: Datastore):  # <- Datastore is azureml.core.Datastore
        ...

    def my_func(self):
        from azureml.core import Datastore
        ...

I import from within the function because there are other classes in the same file that should be imported when not using the extras (extras being azureml).

So this obviously fails, because I refer to Datastore before importing it. Removing the Datastore typing from the __init__ method obviously solves the problem.

So in general my question is whether it is possible, and if so how, to use typing when typing an optional (extras) package.

Notice, that importing in the class definition (below the class MyClass statement) is not a valid solution, as this code is called when the module is imported

Asked By: bluesummers

||

Answers:

You can use TYPE_CHECKING:

A special constant that is assumed to be True by 3rd party static type
checkers. It is False at runtime.

It is False at runtime: So it doesn’t affect your module’s behavior.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from azureml.core import Datastore


class MyClass:
    def __init__(self, datastore: Datastore):
        ...

    def my_func(self):
        from azureml.core import Datastore
        ...

Since I want to show this in action, I will use operator.itemgetter as an instance because it’s recognizable for type checkers, but azureml.core is not:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from operator import itemgetter


class MyClass:
    def __init__(self, datastore: itemgetter):
        ...

    def my_func(self):
        from operator import itemgetter
        ...


obj1 = MyClass(itemgetter(1))  # line 16
obj2 = MyClass(10)             # line 17

Here is the Mypy error:

main.py:17: error: Argument 1 to "MyClass" has incompatible type "int"; expected "itemgetter[Any]"
Found 1 error in 1 file (checked 1 source file)

Which shows it works as excepted.

Answered By: S.B

Just to add my two cents:

While it is certainly a solution, I consider the use of the TYPE_CHECKING constant a red flag regarding the project structure. It typically (though not always) either shows the presence of circular dependencies or poor separation of concerns.

In your case it seems to be the latter, as you state this:

I import from within the function because there are other classes in the same file that should be imported when not using the extras

If MyClass provides optional functionality to your package, it should absolutely reside in its own module and not alongside other classes that provide core functionality.

When you put MyClass into its own module (say my_class), you can place its dependencies at the top with all the other imports. Then you put the import from my_class inside a function that handles the logic of loading internal optional dependencies.

Aside from visibility and arguably better style, one advantage of such a setup over the one you presented is that the my_class module will be consistent in itself and fail on import, if the extra azureml dependency is missing (or broken/renamed/deprecated), rather than at runtime only when MyClass.my_func is called.

You’d be surprised how easy it is to accidentally forget to install all extra dependencies (even in a production environment). Then you’ll thank the stars, when the code fails immediately and transparently, rather than causing errors at some point later at runtime.

Answered By: Daniil Fajnberg