Repository navigation
Conditional import of a class follows imports of all branches #19009
Copy link
Copy link
Closed
Labels
bugmypy got something wrongmypy got something wrongpendingIssues that may be closedIssues that may be closedtopic-reachabilityDetecting unreachable codeDetecting unreachable code
Description
Bug Report
When you have the following provider module:
if sys.platform == "win32":
from my_pkg._pty.windows import PtySession
else:
from my_pkg._pty.unix import PtySessionand attempt importing this symbol from somewhere else, Mypy does not respect the platform condition.
For example, if you call pty.openpty() inside my_pkg._pty.unix and run Mypy on Windows it will show:
Module has no attribute "openpty" [attr-defined]
To Reproduce
Create the following structure:
mypy-issue
├── pyproject.toml
└── pkg
├── __init__.py
├── main.py
└── _pty
├── __init__.py
├── interface.py
├── session.py
├── unix.py
└── windows.py
with the following contents (__init__.py files are empty):
pyproject.toml |
|---|
[tool.mypy]
pretty = true |
pkg/main.py |
|---|
from __future__ import annotations
class SubprocessRunner:
def run(self) -> bool:
from ._pty.session import PtySession
return PtySession is not None |
pkg/_pty/interface.py |
|---|
from __future__ import annotations
from abc import ABC, abstractmethod
class PtySessionInterface(ABC):
def __init__(self, command: list[str]) -> None:
self.command = command
@abstractmethod
def get_exit_code(self) -> int | None: ... |
pkg/_pty/session.py |
|---|
from __future__ import annotations
import sys
if sys.platform == "win32":
from .windows import PtySession
else:
from .unix import PtySession
__all__ = ["PtySession"] |
pkg/_pty/unix.py |
|---|
from __future__ import annotations
import pty
from .interface import PtySessionInterface
class PtySession(PtySessionInterface):
def __init__(self, command: list[str]) -> None:
super().__init__(command)
self._fd, self._child_fd = pty.openpty()
def get_exit_code(self) -> int | None:
return None |
pkg/_pty/windows.py |
|---|
from __future__ import annotations
from .interface import PtySessionInterface
class PtySession(PtySessionInterface):
def __init__(self, command: list[str]) -> None:
super().__init__(command)
self._fd, self._child_fd = (0, 0)
def get_exit_code(self) -> int | None:
return None |
Finally, enter the mypy-issue directory and run mypy pkg on Windows:
$ mypy pkg
pkg\_pty\unix.py:12: error: Module has no attribute "openpty" [attr-defined]
self._fd, self._child_fd = pty.openpty()
^~~~~~~~~~~
Found 1 error in 1 file (checked 7 source files)
Expected Behavior
I would expect that Mypy respects the platform condition like it does in other circumstances I've experienced and is documented.
Your Environment
- Mypy version used: mypy 1.15.0 (compiled: yes)
- Python version used: 3.12
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugmypy got something wrongmypy got something wrongpendingIssues that may be closedIssues that may be closedtopic-reachabilityDetecting unreachable codeDetecting unreachable code
Activity
JelleZijlstra commentedon May 1, 2025
Thanks for putting this together, and sorry I didn't notice the below looking at previous iterations of it.
The
sys.platformchecks works as expected, the mismatch with your expectations is that mypy still type checks every file in the directory, even if it's not imported on that platform. To fix this you'll have to (I believe) wrap the whole body ofunix.pyin anotherif sys.platformblock.ofek commentedon May 1, 2025
Thanks for the quick response! Your answer perplexes me. Are you saying that Mypy doesn't check what code is actually executed based on conditionals and missing attributes like this don't often occur in the wild because libraries typically set to
Nonerather than not assigning a symbol at all? And if they did, we would see this more frequently? What about standard library modules that only exist on certain platforms?sterliakov commentedon May 1, 2025
No, Jelle says that invoking
mypy pkgchecks all files inpkg. So it will checkpkg/_pty/windows.pyon linux and vice versa. You can either exclude these files depending on the platform (no built-in way to do that AFAIC, write a bash wrapper), wrap the whole file withif sys.platform == ...guard or use a file-level assertion (see #5308).If there are no related regressions, just
after imports in
pkg/_pty/windows.pyand equivalent assert inpkg/_pty/unix.pyshould do the trick.sterliakov commentedon May 1, 2025
(note that you'll need to actually run
mypyin CI on different platforms and/or with different--platformflags separately to check all that code, otherwise errors in files intended for other platforms will pass unnoticed)ofek commentedon May 1, 2025
The file-level assertion worked! Is it possible to achieve this with a comment instead so that one doesn't have to potentially unnecessarily
import sysand immediately assert, often messing up the import blocks?sterliakov commentedon May 1, 2025
None that I'm aware of, but you don't have to mess up the imports - unless some of your imports are only available on certain platforms, you can
assertright after all imports.sterliakov commentedon May 1, 2025
Oh, and you may also try
--always-trueand--always-falsefeature to avoid importingsysaltogether, but that might be less trivial to implement - you still have to define/import names you intend to use that way.ofek commentedon May 1, 2025
I tried inline configuration but it looks like this has no effect:
sterliakov commentedon May 1, 2025
Yes, the manual section for the
platformflags says:All such global flags cannot be set inline. And it really makes little sense to set a platform per-file: you can't guarantee that no other file (or even external users, underscores don't prevent me from importing that file) imports it on another platform or without such guard.
ofek commentedon May 1, 2025
Thanks for the help! I'm going to close this since there is nothing to be done (I think?) and will be watching the feature request that I just opened: #19013