python/mypy

mypy bug with try/except conditional imports

timabbott opened this issue Β· 34 comments

try:
    import simplejson
except ImportError:
    import json as simplejson

resulst in the error error: Name 'simplejson' already defined

Pretty sure this is a dup, but I can't find the issue.

Related to #649.

Note that #649 was closed; a buch of "leftover" bugs were opened in its
place, but I don't see one that matches this pattern, so I think it's a new
case.

On Mon, Jan 25, 2016 at 3:19 PM, Ryan Gonzalez notifications@github.com
wrote:

Related to #649 #649.

β€”
Reply to this email directly or view it on GitHub
#1153 (comment).

--Guido van Rossum (python.org/~guido)

Closed by mistake.

Another example from #2251 (reported by @RitwikGupta):

try:
    # Python 3
    from urllib.request import urlopen
except ImportError:
    # Python 2
    from urllib2 import urlopen

And another from #2253:

This python code generates an error. It's a common idiom and it would be good if mypy could check for it in some way.

try:
    import cPickle as pickle
except ImportError:
    import pickle
$ mypy --py2 --silent-imports bad.py
bad.py:4: error: Name 'pickle' already defined

The current workaround is to add a # type: ignore comment. For example:

try:
    import cPickle as pickle
except ImportError:
    import pickle  # type: ignore     # <<-- add this

FYI, this doesn't appear to work with sub-modules:

# This will error
try:
    import foo.bar
except ImportError:
    foo = None  # type:ignore

This seems to work though:

# This will pass
try:
    import doesnt.exist  # type:ignore
except ImportError:
    doesnt = None

# So will this
try:
    import collections.abc  # type:ignore
except ImportError:
    collections = None

assert doesnt is None
assert collections is not None

This is particularly difficult when the import that's happening is a typing import.

For example, this works:

$ mypy --version
mypy 0.530

$ cat no_try.py
from typing import Dict, Any

JSON = Dict[str, Any]

def accept(obj: JSON) -> Any:
    return obj.pop('hello')

$ mypy --ignore-missing-imports no_try.py
$ cat with_try.py
try:
    from typing import Dict, Any
except:
    from backports.typing import Dict, Any  # type: ignore

JSON = Dict[str, Any]

def accept(obj: JSON) -> Any:
    return obj.pop('hello')

$ mypy --ignore-missing-imports with_try.py
with_try.py:10: error: Invalid type "with_try.JSON"
with_try.py:11: error: JSON? has no attribute "pop"

This program should typecheck.

@quodlibetor I don't think your particular case is a bug. mypy has special treatment of typing module, so your imports should be written as:

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from typing import Dict, Any
else:
    try:
        from typing import Dict, Any
    except ImportError:
        from backports.typing import Dict, Any

If your version of typing doesn't have TYPE_CHECKING, then use:

MYPY = False
if MYPY:
    # special imports from typing
else:
    # actual runtime implementation

mypy will understand this.

Hmm, okay, I've verified that that works.

$ cat with_try.py
MYPY = False
if MYPY:
    from typing import Dict, Any
else:
    try:
        from typing import Dict, Any
    except:
        from backports.typing import Dict, Any
JSON = Dict[str, Any]
def accept(obj: JSON) -> Any:
    return obj.pop('hello')

$ mypy with_try.py

I think that I would still be inclined to describe the current behavior as a bug that has a workaround. Would you agree that it is at least as a UX problem?

The if MYPY trick brings the total number of lines of boilerplate to type check a module up to 8, for what would be a single-line import if you were only supporting the most modern python. Even just supporting python 3.5 if you want new items from the typing module means that we jump from 1 to 8 lines of code. It's not even "simple" code. It's also not the obvious first way that I would write that code... obviously.

Why do you need backports.typing? The typing module on PyPI supports all Python versions that mypy itself supports.

I did not know about the typing package on pypi. The last time I looked the only option was backports.typing. That that really ought to fix all my problems, or at least cause interesting new problems ;-) Thank you!

Steap commented

I recently stumbled upon a similar issue with the following code snippet:

try:
    from yaml import CLoader as Loader
except ImportError:
    from yaml import Loader

The error message is different though, so it may actually be a different issue:

$ mypy /tmp/bug.py
/tmp/bug.py:4: error: Incompatible import of "Loader" (imported name has type "Type[Loader]", local name has type "Type[CLoader]")

This happens with mypy 0.670+dev.d5cf72b141ce93108c28864ef084c936964ae152 .

Any thoughts?

This is the same problem, but with a class instead of module. If you are not using Loader in annotations, then you can probably work around this by placing:

from typing import TYPE_CHECKING, Union, Type
if TYPE_CHECKING:
    import yaml
    Loader: Union[yaml.Cloader, yaml.Loader]

above the try statement. But again this may only work if you are not using Loader in annotations elsewhere in the file.

Is there any suggestion of how to approach this for a definitive fix? Is it a mypy-related bug?

It seems to me as a not-so-rare case, given libraries might use except ImportError in order to provide fallbacks.

I would certainly rather not having any type: ignore annotation on my code base

Is there any suggestion of how to approach this for a definitive fix? Is it a mypy-related bug?

What do you mean by a "definitive fix"? Do you mean actually allow this in mypy? If yes, then this is non-trivial, definitely would not recommend this as a first issue.

Yes, that's what I mean. Not considering taking it as a first issue though, I've got my plate full already with #6568 on my mypy-spare time

Any hope of a fix any time soon? It's been nearly four years...

I recently started adding type annotations to an existing, large, mature project, and it has dozens of these try/import/except/alternative examples, and adding # type: ignore to all of them feels really gross...

I can't speak for the maintainers (nor do I want to), but my guess is that this is a relatively low priority in light of sunsetting Python 2. I have no doubt that there are legitimate cases of conditional imports of API-compatible libraries that don't involve writing cross-version compatible code, but my guess is that a majority of the uses of this technique are specifically for writing libraries that are both Python 2 and Python 3 compatible, which (as I understand) is now discouraged.

Again, just a guess though….

The majority of my cases are actually not about supporting one version of Python or another, but rather supporting different versions of different libraries or supporting the existence or non-existence of a third-party library and using or not using alternatives to those.

I agree that this is not restricted to dealing Python 2/3 compatibility issues, so this will continue to be relevant after sunsetting Python 2. The main reason that this is still unsupported is that the fix is non-trivial to implement, and the core team has their plate full with other work. If somebody wants to try fixing this, I'm happy to give hints and pointers.

Perhaps related? Code blocks like this:

try:
    import pandas as pd
except ImportError:
    pd = None

Can't seem to stop mypy complaining about incompatible types here what I try to do ('(expression has type "None", variable has type Module').

@aldanor, here's a workaround, as mentioned above:

try:
    import pandas as pd
except ImportError:
    pd = None  # type: ignore

There is no better way to deal with this error as of now.

We encountered this in Twine with importlib.metadata vs importlib_metadata, but only as of mypy 0.750. There's a fix pending in pypa/twine#551 (which also covers an instance of #1393).

I made a repository to reproduce the errors and try out some fixes: https://github.com/bhrutledge/mypy-importlib-metadata. That's currently passing, but the commit history has more details.

Re: my previous comment, there's a suggested workaround (using sys.version) and related discussion starting at #1393 (comment). I applied the workaround in pypa/twine#551.

The sys.version workaround was introduced here: #698

A pattern that might be nicer for some?

if TYPE_CHECKING or sys.version_info < (3, 8, 0):
    from typing_extensions import Literal
else:
    from typing import Literal

Building on an answer from @cjerdonek, would this be an acceptable workaround instead of # type: ignore?:

try:
    from py3_pkg import module as _module
except:
    from py2_pkg import module
else:
    module = _module

Is this going to be fixed anytime soon?

No, it's not, and it's not clear that it should.

A majority of cases I've seen here are to work around Python stdlib changes, especially Python 2 to Python 3 stuff. sys.version_info checks are the preferred way to do this. Also Python 2 is dead.

if sys.version_info >= (3, 8):
    import importlib.metadata as importlib_metadata
else:
    import importlib_metadata

For the remaining cases, it's often not clear that it's sound, especially given nominal typing. Even if the API is structurally the same, nominal isinstance checks could result in surprising behaviour from mypy.

The only thing we should do in this space is #5018 where you can explicitly define exactly the structure you want with a Protocol, and mypy will confirm.

For all other cases, if you want to lie to the typechecker, please, go ahead and lie to the type checker (or type ignore):

if TYPE_CHECKING:
    import json
else:
    try:
        import simplejson as json
    except ImportError:
        import json

FYI "type ignore" doesn't work well because in some cases it will cause Unused "type: ignore" comment errors elsewhere (e.g. GitHub Actions).

@lilydjwg Issue #8823 was fixed last week and covers what you just described - unused-ignore errors in unreachable code.