Skip to content
Open
28 changes: 25 additions & 3 deletions docs/source/error_code_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,8 @@ Check for an issue with imports [import]
----------------------------------------

Mypy generates an error if it can't resolve an `import` statement.
This is a parent error code of `import-not-found` and `import-untyped`
This is a parent error code of `import-not-found`, `import-untyped`,
and `import-untyped-stubs-available`.

See :ref:`ignore-missing-imports` for how to work around these errors.

Expand All @@ -693,7 +694,7 @@ See :ref:`ignore-missing-imports` for how to work around these errors.
.. _code-import-untyped:

Check that import target can be found [import-untyped]
--------------------------------------------------------
------------------------------------------------------

Mypy generates an error if it can find the source code for an imported module,
but that module does not provide type annotations (via :ref:`PEP 561 <installed-packages>`).
Expand All @@ -702,14 +703,35 @@ Example:

.. code-block:: python

# Error: Library stubs not installed for "bs4" [import-untyped]
# Error: Library stubs not installed for "bs4" [import-untyped-stubs-available]
import bs4
# Error: Skipping analyzing "no_py_typed": module is installed, but missing library stubs or py.typed marker [import-untyped]
import no_py_typed

In some cases, these errors can be fixed by installing an appropriate
stub package. See :ref:`ignore-missing-imports` for more details.

.. _code-import-untyped-stubs-available:

Check that import target with known stubs can be found [import-untyped-stubs-available]
---------------------------------------------------------------------------------------

Like :ref:`code-import-untyped`, but used when mypy knows there is an appropriate
type stub package corresponding to the library, which you could install.

Example:

.. code-block:: python

# Error: Library stubs not installed for "bs4" [import-untyped-stubs-available]
import bs4
# Error: Skipping analyzing "no_py_typed": module is installed, but missing library stubs or py.typed marker [import-untyped]
import no_py_typed

These errors can be fixed by installing the appropriate
stub package. See :ref:`ignore-missing-imports` for more details.


.. _code-no-redef:

Check that each name is defined once [no-redef]
Expand Down
4 changes: 3 additions & 1 deletion docs/source/running_mypy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,9 @@ This is slower than explicitly installing stubs, since it effectively
runs mypy twice -- the first time to find the missing stubs, and
the second time to type check your code properly after mypy has
installed the stubs. It also can make controlling stub versions harder,
resulting in less reproducible type checking.
resulting in less reproducible type checking — it might even install
incompatible versions of your project's non-type dependencies, if the
type stubs require them!

By default, :option:`--install-types <mypy --install-types>` shows a confirmation prompt.
Use :option:`--non-interactive <mypy --non-interactive>` to install all suggested
Expand Down
7 changes: 3 additions & 4 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2783,11 +2783,10 @@ def module_not_found(
msg, notes = reason.error_message_templates(daemon)
if reason == ModuleNotFoundReason.NOT_FOUND:
code = codes.IMPORT_NOT_FOUND
elif (
reason == ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS
or reason == ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED
):
elif reason == ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS:
code = codes.IMPORT_UNTYPED
elif reason == ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED:
code = codes.IMPORT_UNTYPED_STUBS_AVAILABLE
else:
code = codes.IMPORT
errors.report(line, 0, msg.format(module=target), code=code)
Expand Down
9 changes: 9 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def __init__(
sub_code_map[sub_code_of.code].add(code)
error_codes[code] = self

def is_import_related_code(self) -> bool:
return IMPORT in (self.code, self.sub_code_of)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Informtional:) This could easily be generalized to is_related_code, but hey: he who actually wants that feature is free to do that refactor, YAGNI, etc.


def __str__(self) -> str:
return f"<ErrorCode {self.code}>"

Expand Down Expand Up @@ -113,6 +116,12 @@ def __hash__(self) -> int:
IMPORT_UNTYPED: Final = ErrorCode(
"import-untyped", "Require that imported module has stubs", "General", sub_code_of=IMPORT
)
IMPORT_UNTYPED_STUBS_AVAILABLE: Final = ErrorCode(
"import-untyped-stubs-available",
"Require that imported module (with known stubs) has stubs",
"General",
sub_code_of=IMPORT,
)
NO_REDEF: Final = ErrorCode("no-redef", "Check that each name is defined once", "General")
FUNC_RETURNS_VALUE: Final = ErrorCode(
"func-returns-value", "Check that called function returns a value in value context", "General"
Expand Down
6 changes: 3 additions & 3 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from mypy import errorcodes as codes
from mypy.error_formatter import ErrorFormatter
from mypy.errorcodes import IMPORT, IMPORT_NOT_FOUND, IMPORT_UNTYPED, ErrorCode, mypy_error_codes
from mypy.errorcodes import ErrorCode, mypy_error_codes
from mypy.nodes import Context
from mypy.options import Options
from mypy.scope import Scope
Expand Down Expand Up @@ -583,7 +583,7 @@ def _add_error_info(self, file: str, info: ErrorInfo) -> None:
self.error_info_map[file].append(info)
if info.blocker:
self.has_blockers.add(file)
if info.code in (IMPORT, IMPORT_UNTYPED, IMPORT_NOT_FOUND):
if info.code is not None and info.code.is_import_related_code():
Copy link
Contributor Author

@wyattscarpenter wyattscarpenter Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Informational:) this code pattern also suggests refactoring into a static function (so it can just handle None as well instead of needing this check), but I didn't because YAGNI; this was fine.

self.seen_import_error = True

def get_watchers(self) -> Iterator[ErrorWatcher]:
Expand Down Expand Up @@ -630,7 +630,7 @@ def add_error_info(self, info: ErrorInfo) -> None:
self.only_once_messages.add(info.message)
if (
self.seen_import_error
and info.code not in (IMPORT, IMPORT_UNTYPED, IMPORT_NOT_FOUND)
and (info.code is None or (not info.code.is_import_related_code()))
and self.has_many_errors()
):
# Missing stubs can easily cause thousands of errors about
Expand Down
2 changes: 1 addition & 1 deletion mypy/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from mypy.version import __version__

try:
from lxml import etree # type: ignore[import-untyped]
from lxml import etree

LXML_INSTALLED = True
except ImportError:
Expand Down
4 changes: 3 additions & 1 deletion mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import tempfile
from pathlib import Path
from types import ModuleType

from mypy import build
from mypy.errors import CompileError
Expand All @@ -25,8 +26,9 @@
)
from mypy.test.update_data import update_testcase_output

lxml: ModuleType | None # lxml is an optional dependency
try:
import lxml # type: ignore[import-untyped]
import lxml
except ImportError:
lxml = None

Expand Down
4 changes: 3 additions & 1 deletion mypy/test/testcmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import re
import subprocess
import sys
from types import ModuleType

from mypy.test.config import PREFIX, test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
Expand All @@ -19,8 +20,9 @@
normalize_error_messages,
)

lxml: ModuleType | None # lxml is an optional dependency
try:
import lxml # type: ignore[import-untyped]
import lxml
except ImportError:
lxml = None

Expand Down
6 changes: 4 additions & 2 deletions mypy/test/testreports.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from __future__ import annotations

import textwrap
from types import ModuleType

from mypy.report import CoberturaPackage, get_line_rate
from mypy.test.helpers import Suite, assert_equal

lxml: ModuleType | None # lxml is an optional dependency
try:
import lxml # type: ignore[import-untyped]
import lxml
except ImportError:
lxml = None

Expand All @@ -23,7 +25,7 @@ def test_get_line_rate(self) -> None:

@pytest.mark.skipif(lxml is None, reason="Cannot import lxml. Is it installed?")
def test_as_xml(self) -> None:
import lxml.etree as etree # type: ignore[import-untyped]
import lxml.etree as etree

cobertura_package = CoberturaPackage("foobar")
cobertura_package.covered_lines = 21
Expand Down
8 changes: 7 additions & 1 deletion test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,13 @@ if int() is str(): # E: Non-overlapping identity check (left operand type: "int
[builtins fixtures/primitives.pyi]

[case testErrorCodeMissingModule]
from defusedxml import xyz # E: Library stubs not installed for "defusedxml" [import-untyped] \
-- Note: it was too difficult for me to figure out how to test [import-untyped] here,
-- (ideally, it would!)
-- but testNamespacePkgWStubs does test that, anyway.
-- TODO: can this be done? The specific error message is
-- Skipping analyzing "no_py_typed": module is installed, but missing library stubs or py.typed marker [import-untyped]
-- which apparently was never tested for non-namespace packages before...
from defusedxml import xyz # E: Library stubs not installed for "defusedxml" [import-untyped-stubs-available] \
# N: Hint: "python3 -m pip install types-defusedxml" \
# N: (or run "mypy --install-types" to install all missing stub packages)
from nonexistent import foobar # E: Cannot find implementation or library stub for module named "nonexistent" [import-not-found]
Expand Down
1 change: 1 addition & 0 deletions test-requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
attrs>=18.0
filelock>=3.3.0,<3.20.0 # latest version is not available on 3.9 that we still support
lxml>=5.3.0; python_version<'3.15'
lxml-stubs>=0.5.1
psutil>=4.0
pytest>=8.1.0
pytest-xdist>=1.34.0
Expand Down
2 changes: 2 additions & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ librt==0.4.0
# via -r mypy-requirements.txt
lxml==6.0.2 ; python_version < "3.15"
# via -r test-requirements.in
lxml-stubs>=0.5.1
# via -r test-requirements.in
mypy-extensions==1.1.0
# via -r mypy-requirements.txt
nodeenv==1.9.1
Expand Down