python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.55k stars 2.84k forks source link

A 'type only' module import should not count as importing a module #11503

Open KotlinIsland opened 3 years ago

KotlinIsland commented 3 years ago

given:

my-app/
├─ package/
│  ├─ __init__.py
│  ├─ things.py
├─ entry.py
├─ mod.py

entry.py:

import mod
import package

mod.func(package.things.Thing())

mod.py:

def func(it):
    ...

In typeshed/bundled stubs: mod.pyi

from package.things import Thing

def func(it: Thing) -> None: ...
OR, if this is fake imported in the .py module: mod.py ```py from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING # do a type import so doesn't cause circular imports or slow down loading from package.things import Thing def func(it: Thing) -> None: ... ```

package.things.py

class Thing:
    ...

When mypy --strict entry.py is executed, no error is generated. When python entry.py is run: AttributeError: module 'package' has no attribute 'things'

Mypy treats the 'type import' as importing package.things, when in reality it isn't. I understand this is consistent with what TYPE_CHECKING should do, but seems really sus if you ask me.

This more seriously affects typeshed, where all imports are fake and mypy will think all fakely imported modules are really imported.

KotlinIsland commented 3 years ago

like, in a stub there is no way so specify if a module is actually imported at runtime, or if you are just trying to access a type for type time.

erictraut commented 3 years ago

The TYPE_CHECKING symbol allows you to "lie" to the type checker, effectively making it see something different from the interpreter at runtime. I warn users that they use TYPE_CHECKING at their own peril because these differences can lead to inconsistencies. Your sample above is one such example but there are many other similar patterns. My recommendation is to avoid using TYPE_CHECKING if at all possible. For example, using it to avoid circular imports is a really bad justification, IMO. The better approach is to refactor your code to avoid the circular dependency.

In a stub, there is a way to specify that the symbol is exported at runtime. PEP 484 documents a mechanism for this. You do so by using a redundant alias form of import, such as from package.things import Thing as Thing.

KotlinIsland commented 3 years ago

@erictraut I understand those limitations and agree, it was more just to explain the issue. (although an import type would be very appreciated)

You haven't got the issue with stubs quite right though.

The issue isn't that Thing is exported or not, look at the example, It's not referencing the Thing from mod it's accessing it from package.things.

The issue is that mypy thinks package.things is imported(because the stub says from package.things import Thing), when it isn't, leading to AttributeError: module 'package' has no attribute 'things'

There is no way to specify in a stub that your import is reflected or not at runtime.