Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mypy "Incompatible import" error for conditional imports

I have the following code

try:
    from mypackage.optional.xxx import f1, f2
except ImportError:
    from mypackage.optional.yyy import f1, f2

The modules xxx and yyy provide the same functionality but the functions are coded very differently and accept a different input type and based on different external libraries (which are optional dependencies of my package).

Unfortunately mypy is complaining:

error: Incompatible import of "f1" (imported name has type "Callable[[Arg(Any, 'yyyarg1')], Any]", local name has type "Callable[[Arg(Any, 'xxxarg1')], Any]")

How can I solve this issue? What is the best way to import the same functionality (i.e. same function names with similar signatures) conditionally?

like image 315
RMeli Avatar asked Nov 25 '19 17:11

RMeli


1 Answers

The problem here is that mypy is somewhat picky about the two imports -- the two libraries need to have identical APIs before mypy will be satisfied.

This includes any parameter names, since keyword arguments are a thing: doing f1(xxxarg1=blah) will work for the first import, but not for the latter.

(The fix for this specific case would be to either (a) make your params the same name, (b) use positional-only arguments which are available only in Python 3.8+, or (c) prefix your param names with two underscores which is a mypy-specific way of declaring a param to be positional only -- but this strategy works for all Python versions.)

Personally, I think making the signatures of the two functions identical is the best option, since it helps minimize the chances your code will have subtle bugs/reduce the amount of testing you need to do.

But if modifying your APIs to be identical isn't feasible, you could either suppress the error or try and get mypy to type-check your imports more precisely via a combination of:

  • The typing.TYPE_CHECKING variable, which is always False at runtime but treated as being always true by mypy
  • ...and perhaps the --always-true/--always-false command line flags, which let you tell mypy to assume that some variable is always true or false.

In total, there are three different approaches I'm aware of that you can take:

Approach 1: suppress any errors with the second import

First, if the two libraries have nearly identical APIs and you don't care about any small differences between the two, one strategy might be to just type-ignore the latter import, which will make mypy suppress any errors originating on that final line.

Type checking for all other lines will be unaffected, which means mypy will continue to assume that f1 and f2 were imported from xxx.

try:
    from mypackage.optional.xxx import f1, f2
except ImportError:
    from mypackage.optional.yyy import f1, f2  # type: ignore

This type-ignore option is probably the most pragmatic approach.

Approach 2: explicitly pick the first import, ignoring the second

Alternatively, if you dislike ignoring anything, you could perhaps instead make mypy just ignore that import entirely by doing:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Ignored at runtime, but not by mypy
    from mypackage.optional.xxx import f1, f2
else:
    # Ignored by mypy, but not at runtime
    try:
        from mypackage.optional.xxx import f1, f2
    except ImportError:
        from mypackage.optional.yyy import f1, f2

Doing if False: ... else: ... also works, though it makes the code a bit more cryptic.

One important thing to note is that both this approach and the type-ignore approach are exactly the same in type safety/unsafety. You would mainly pick this approach if you want to be a little more explicit about what you're doing or want to avoid ignores at all costs.

Approach 3: type check both variants

The third and final option would be to run mypy twice, once per each library using the --always-true/--always-false flags. This would be the most type-safe and rigorous option.

For example, you could do:

from typing import TYPE_CHECKING

# Actual runtime logic
if not TYPE_CHECKING:
    # Ignored by mypy, but not at runtime
    try:
        from mypackage.optional.xxx import f1, f2
        USES_XXX = True
    except ImportError:
        from mypackage.optional.yyy import f1, f2
        USES_XXX = False

# For the benefit of mypy
if TYPE_CHECKING:
    if USES_XXX:
        from mypackage.optional.xxx import f1, f2
    else:
        from mypackage.optional.yyy import f1, f2

...then run both mypy --always-true=USES_XXX your_code and mypy --always-false=USES_XXX your_code.

like image 92
Michael0x2a Avatar answered Oct 02 '22 07:10

Michael0x2a