Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pyright doesn't respect the documented resolution order #9752

Open
aaugustin opened this issue Jan 24, 2025 · 5 comments
Open

pyright doesn't respect the documented resolution order #9752

aaugustin opened this issue Jan 24, 2025 · 5 comments
Labels
addressed in next version Issue is fixed and will appear in next published version bug Something isn't working

Comments

@aaugustin
Copy link

Describe the bug

Here are minimal reproduction instructions for an issue that affects https://github.com/python-websockets/websockets.

  1. Create these two files.
  • mymodule/__init__.py
import typing

from .typing import Data


if typing.TYPE_CHECKING:
    ...
else:
    ...
  • mymodule/typing.py
from typing import Union

Data = Union[str, bytes]
  1. Run pyright
$ pyright mymodule
/Users/myk/Desktop/mymodule/__init__.py
  /Users/myk/Desktop/mymodule/__init__.py:6:11 - error: "TYPE_CHECKING" is not a known attribute of module "typing" (reportAttributeAccessIssue)
1 error, 0 warnings, 0 informations

Expected behavior

No error is raised. The code is valid.

Actual behavior

pyright raises the error shown above.

Analysis

pyright believes incorrectly that typing is myproject.typing rather than the stdlib's typing module.

Indeed, adding reveal_type(typing.Data) in __init__.py gives the following incorrect result:

/Users/myk/Desktop/mymodule/__init__.py:6:13 - information: Type of "typing.Data" is "type[str] | type[bytes]"

Based on the import resolution docs, I expect pyright to find the stdlib module at step 4. However, it appears to continue until step 6.

I understand why step 6 exists as a last resort — it mimicks Python adding the directory containing a script to the import path. I don't understand why it takes precedence over step 4.

I checked the search paths in the verbose output and confirmed that there's a typing.pyi stub defining the TYPE_CHECKING attribute in the first path of the list. This should match at step 4.

$ pyright --verbose mymodule
Auto-excluding **/node_modules
Auto-excluding **/__pycache__
Auto-excluding **/.*
Execution environment: python
  Extra paths:
    (none)
  Python version: 3.13
  Python platform: Darwin
  Search paths:
    /Users/myk/.local/pipx/venvs/pyright/lib/python3.13/site-packages/pyright/dist/dist/typeshed-fallback/stdlib
    /Users/myk/Desktop
    /Users/myk/Desktop/typings
    /Users/myk/.local/pipx/venvs/pyright/lib/python3.13/site-packages/pyright/dist/dist/typeshed-fallback/stubs/...
    /Users/myk/.pyenv/versions/3.13.1/lib/python3.13
    /Users/myk/.pyenv/versions/3.13.1/lib/python3.13/lib-dynload
    /Users/myk/.pyenv/versions/3.13.1/lib/python3.13/site-packages
Found 2 source files
pyright 1.1.392
/Users/myk/Desktop/mymodule/__init__.py
  /Users/myk/Desktop/mymodule/__init__.py:8:11 - error: "TYPE_CHECKING" is not a known attribute of module "typing" (reportAttributeAccessIssue)
1 error, 0 warnings, 1 information
Completed in 0.309sec
$ grep TYPE_CHECKING: /Users/myk/.local/pipx/venvs/pyright/lib/python3.13/site-packages/pyright/dist/dist/typeshed-fallback/stdlib/typing.pyi
TYPE_CHECKING: bool

I looked for duplicate issues and found #7226. However, my issue is different. I can live with step 6 of the import resolution happening. I just need step 4 to take precedence over step 6, which appears to be your intent, but isn't the result I'm getting with this very basic example.

@aaugustin
Copy link
Author

For the sake of completeness — after removing mymodule/typing.py, pyright types the following module flawlessly:

import typing

if typing.TYPE_CHECKING:
    ...
else:
    ...

This proves that it can find the stdlib typing module. The problem is in the precedence order, as far as I can tell.

I attempted to create a config file in the hope of teaching pyright where the root path was, so that it would stop the incorrect import, but I could never change the outcome.

@erictraut
Copy link
Collaborator

Pyright is working as intended here, so I don't consider this a bug.

When you use a relative import statement (e.g. from .x from y) in a __init__.py file, the import loader will resolve the submodule x and then cache the resulting module object in a global variable x within the __init__.py module. In your case, the x symbol is replaced by typing, and the typing symbol which was assigned in the import typing statement is subsequently overwritten by the from .typing import Data statement. Note that this affects only __init__.py files, not other modules.

You can see this if you add print statements to your code:

import typing
print(typing)

from .typing import Data
print(typing)

Now create a dummy file called test.py and add the statement import mymodule, and execute it (python test.py), you'll see the following output:

<module 'typing' from '/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py'>
<module 'mymodule.typing' from '<redacted>/mymodule/typing.py'>

@erictraut erictraut closed this as not planned Won't fix, can't repro, duplicate, stale Jan 24, 2025
@erictraut erictraut added the as designed Not a bug, working as intended label Jan 24, 2025
@aaugustin
Copy link
Author

aaugustin commented Jan 24, 2025

Ah, yes, now it's obvious to me why __init__.py modules have this behavior. That's what I had missed.


FYI, in my real code, the relative import comes after accessing typing.TYPE_CHECKING, which is why the code works:

import typing

if typing.TYPE_CHECKING:
    from .typing import Data
else:
    ... # lazy import of Data

To sum up, while the following code works, pyright considers that it isn't properly typed, essentially because the second import shadows the first one:

import typing
print(typing.TYPE_CHECKING)

from .typing import Data
print(typing.Data)

Raising the error message at the level of typing.TYPE_CHECKING when the real problem is from .typing import Data made it difficult for me to figure out what was going on.

Note that pyright will happily accept the following code, even though it crashes with an AttributeError:

import typing
print(typing.Data)
from .typing import Data

Ideally, there would be a correlation between pyright declaring error: "xxx" is not a known attribute of module "typing" and python crashing with AttributeError: module 'typing' has no attribute 'xxx' :-)

Depending on how much you care about UX, you may want to make pyright reject that last example. I would find it intuitive to raise an error at the level of from .typing import Data.

@aaugustin
Copy link
Author

PS: I understand that you're not going to change your mind on the design of pyright. I will alias an import to avoid shadowing.

If the above gives you an idea to improve the UX, fantastic; if not, thank you for your fast response — my problem is solved :-)

erictraut added a commit that referenced this issue Jan 24, 2025
… shadow the same symbol and an attribute is accessed between the two import statements. This addresses #9752.
erictraut added a commit that referenced this issue Jan 24, 2025
… shadow the same symbol and an attribute is accessed between the two import statements. This addresses #9752.
erictraut added a commit that referenced this issue Jan 24, 2025
… shadow the same symbol and an attribute is accessed between the two import statements. This addresses #9752.
erictraut added a commit that referenced this issue Jan 24, 2025
… shadow the same symbol and an attribute is accessed between the two import statements. This addresses #9752. (#9755)
@erictraut
Copy link
Collaborator

Yes, the expression typing.Data should result in an error in your example above. That is indeed a bug. It will be fixed in the next release.

@erictraut erictraut reopened this Jan 24, 2025
@erictraut erictraut added addressed in next version Issue is fixed and will appear in next published version and removed as designed Not a bug, working as intended labels Jan 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addressed in next version Issue is fixed and will appear in next published version bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants