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?
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:
typing.TYPE_CHECKING
variable, which is always False at runtime but treated as being always true by mypy--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
.