Fixing import-untyped and Missing Imports
TL;DR — mypy [import-untyped] means the module was found but ships no type information — install a types-* stub package or scope an ignore to that module. mypy [import-not-found] means the module could not be resolved at all — fix the virtualenv, dependency, or path. They look similar in CI logs but demand opposite fixes, and treating one as the other either hides a real bug or wastes an afternoon.
The two errors sit next to each other in mypy output and both mention an import, so it is tempting to reach for ignore_missing_imports and move on. Resist that: one is a typing gap you can safely paper over, the other is a broken environment that ignore_missing_imports will silently mask, leaving you shipping Any-typed code that no checker guards.
Confirming which error you have
Run the import in the same interpreter mypy uses. If python -c "import legacypay" succeeds, the module resolves and any mypy complaint is [import-untyped]. If that command raises ModuleNotFoundError, you have an [import-not-found] situation and no amount of stubbing will help until the import itself works.
$ python -c "import legacypay" && echo OK
OK # → so mypy's complaint is [import-untyped]
$ python -c "import notinstalled"
ModuleNotFoundError: No module named 'notinstalled' # → [import-not-found]
Fixing [import-untyped]
The module runs; it just has no types. Prefer a maintained stub package when one exists:
$ mypy app.py
app.py:3: error: Library stubs not installed for "requests" [import-untyped]
app.py:3: note: Hint: "python3 -m pip install types-requests"
$ pip install types-requests
mypy can even install the suggested stubs for you in CI — non-interactively, so it never blocks a pipeline:
$ mypy --install-types --non-interactive app.py
When no types-* package exists (an internal library, say), scope the suppression to just that module with a per-module override — never globally, which would blind mypy to every third-party gap:
# pyproject.toml — mypy 1.10+
[[tool.mypy.overrides]]
module = ["legacypay.*", "internal_billing.*"]
ignore_missing_imports = true
That tells mypy “treat these specific modules as Any on purpose.” If you would rather generate real types than accept Any, produce stubs instead — the mypy configuration and strictness guide covers where these overrides fit into a broader config.
Fixing [import-not-found]
Here the module genuinely cannot be found, and the cause is environmental:
- Dependency not installed in the environment mypy runs against —
pip installit, and make sure CI installs the project’s real dependencies before checking. - mypy running against the wrong interpreter — a global mypy checking a project whose deps live in a virtualenv. Run
python -m mypyfrom inside the venv so it sharessys.path. - First-party package not on the path — your own
src/layout isn’t discoverable. Setmypy_pathor add the package to the environment withpip install -e ..
# Python 3.12+, checked with mypy 1.10
from internal_billing.ledger import Ledger # error: Cannot find
# implementation or library stub for module "internal_billing.ledger"
# [import-not-found] → the package is not installed in this env
The wrong fix here is ignore_missing_imports = true: it silences the message but leaves Ledger typed as Any, so every downstream [attr-defined] and [arg-type] check on it evaporates.
ModuleNotFoundError at runtime and one that imports fine but lacks stubs are completely different failures — yet suppressing mypy with ignore_missing_imports makes both go quiet. The static tool then reports success while the program may still crash on import. Always reproduce the import in the interpreter before deciding a mypy import error is "just a stubs problem."
The pyright equivalents
pyright splits the same distinction across two diagnostics:
reportMissingModuleSource— pyright found a stub (or types) but not the runtime module, or the module resolves without type info. This is pyright’s rough analogue of[import-untyped]; it is a warning by default, not an error.reportMissingImports— pyright cannot resolve the import at all, the counterpart of mypy’s[import-not-found].
{
"reportMissingModuleSource": "none",
"extraPaths": ["src"]
}
Set reportMissingModuleSource to "none" to accept a known-untyped module, and use extraPaths (pyright’s mypy_path equivalent) to make a first-party src/ layout resolvable so reportMissingImports clears. The full mypy-vs-pyright picture is in pyright vs mypy comparison.
Common mistakes
- Global
ignore_missing_imports = true. It silences[import-untyped]and[import-not-found]everywhere, so a genuinely broken import never surfaces. Scope every ignore to named modules with[[tool.mypy.overrides]]. - Installing a
types-*package for a module that still fails[import-not-found]. Stubs cannot resolve a module that does not import; you fixed the wrong error. Get the runtime import working first. - Checking with a different interpreter than the app uses. A global mypy hits
[import-not-found]on deps installed only in the project venv. Runpython -m mypyinside that venv. - Marking a first-party package untyped instead of adding a
py.typedmarker. For code you own, ship the marker described in shipping inline types with py.typed rather than suppressing it as an untyped third party.
FAQ
Is --install-types --non-interactive safe for CI?
Yes. It makes mypy fetch the types-* stubs it recommends without prompting, which unblocks pipelines. Pin the resulting stub versions in your lockfile so CI stays reproducible across runs.
Why does ignore_missing_imports feel like it “fixes everything”?
Because it converts the offending module to Any, which makes both [import-untyped] and [import-not-found] disappear. That is exactly why it is dangerous globally — it can hide a real unresolved import as easily as a missing stub. Always scope it to specific modules.