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.

Diagnosing import-untyped versus import-not-found Ask whether the module imports at runtime; if yes it is untyped and needs stubs, if no it is unresolved and needs an environment fix. mypy flags an import does it import at runtime? YES → [import-untyped] module exists, no types NO → [import-not-found] cannot resolve module install types-* or scope ignore fix venv / deps / MYPYPATH
The runtime import test splits the two errors cleanly: a module that imports needs types; one that does not needs an environment fix.

Confirming which error you have

The single most reliable diagnostic is to reproduce the import in the same interpreter mypy analyses against. mypy does not execute your code, but it does resolve modules against a Python environment: by default the one whose site-packages it can see, or the one named by python_executable in config. If python -c "import legacypay" succeeds, the module is present on that path, so any remaining mypy complaint is a types problem — [import-untyped]. If the same command raises ModuleNotFoundError, mypy cannot resolve it either, and you are looking at [import-not-found]. No stub package will fix the second case 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]
The runtime-import probe that classifies the error Running python -c import in mypy's own interpreter succeeds into import-untyped or fails into import-not-found. python -c "import m" mypy's own interpreter exit status? 0 or traceback 0 → [import-untyped] module resolves, no types ≠0 → [import-not-found] unresolved on sys.path
The probe's exit status is the classifier: a clean import means a types gap, a traceback means an unresolved module.

The catch is making sure you probe the right interpreter. A globally-installed mypy analyses whatever environment its python_executable points at, which may not be the venv you have activated. Pin it explicitly so the probe and the checker agree:

$ which python; python -c "import sys; print(sys.executable)"
$ python -m mypy app.py          # runs mypy from *this* interpreter

Setting python_executable (or letting mypy default to sys.executable when invoked as python -m mypy) removes the ambiguity:

# pyproject.toml — mypy 1.10+
[tool.mypy]
python_executable = ".venv/bin/python"

Add mypy -v app.py (verbose) when you want to see where mypy looked: it prints each candidate path and whether a py.typed marker, a stub package, or nothing was found — the same four-source chain described in type stubs and third-party packages. One subtlety: follow_imports = skip or silent can suppress the very error you are chasing, so if mypy stays quiet on a module you expected to flag, check that setting before assuming the types are fine.

Fixing [import-untyped]

The module runs; it just has no types. There is a clear ladder of remedies, and you want the highest rung that applies. The top rung is a maintained stub package: when one exists, mypy names it for you in a note: right under the error, so you rarely have to guess.

$ 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"
app.py:3: note: (or run "mypy --install-types" to install all missing stub packages)

$ pip install types-requests
Ladder of remedies for import-untyped Prefer a stub package, then generated stubs, then a scoped ignore, with a global ignore as the discouraged bottom rung. prefer the top rung that applies 1. pip install types-* (maintained, versioned) 2. stubgen → refine local .pyi 3. scoped [[tool.mypy.overrides]] 4. global ignore — avoid
Each rung down trades type safety for convenience; take the highest one your dependency allows.

mypy can install the suggested stubs for you in CI — non-interactively, so it never blocks a pipeline. Since mypy needs to have seen the errors before it knows what to fetch, --install-types alone runs mypy twice; passing the flags together is the CI-safe form:

$ mypy --install-types --non-interactive app.py

Pin whatever it installs in your lockfile so runs stay reproducible — stub packages version independently of the library and a drifting types-* produces phantom [attr-defined] errors for methods your pinned library does not actually have.

When no types-* package exists (an internal library, a niche C extension), the second rung is to generate your own skeletons with stubgen and refine them. If even that is not worth the effort, scope the suppression to just the offending 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.” The .* suffix matches the package and every submodule; without it, legacypay.client would still error. Note the direction of travel over recent versions: mypy used to be silent about untyped third-party imports and only began emitting [import-untyped] as a distinct, actionable code in 1.0 (previously these were folded into a generic note). Under --strict the related disallow_untyped_calls will additionally flag calls into an Any-typed module, so a scoped ignore_missing_imports alone may not fully silence a strict run.

One flag deserves special attention when you accept an untyped import: disallow_any_unimported. It is not part of --strict, so you must enable it deliberately, and it catches a subtle leak — when an ignored module’s Any bleeds into your public signatures. If legacypay is treated as Any and you write def process(client: legacypay.Client) -> None, the parameter silently becomes Any, and every caller loses checking without any warning. With disallow_any_unimported = true mypy reports Argument 1 to "process" becomes "Any" due to an unfollowed import, forcing you to wrap the boundary in a Protocol or a local stub instead of quietly propagating Any through your own API. 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 almost always environmental. Each root cause maps to a specific, different fix — reaching for the wrong one wastes the afternoon the TL;DR warned about:

  • Dependency not installed in the environment mypy runs against — pip install it, and make sure your CI job installs the project’s real dependencies (not just the dev/lint extras) before the type-check step.
  • mypy running against the wrong interpreter — a global mypy checking a project whose deps live in a virtualenv. Run python -m mypy from inside the venv so it shares sys.path, or set python_executable in config.
  • First-party package not on the path — your own src/ layout isn’t discoverable. Set mypy_path or install the package into the environment with pip install -e ..
Cause-to-fix mapping for import-not-found Three root causes each map across to their own remedy: install the dependency, use the venv interpreter, or add the package to the path. root cause the fix dep not installed in this env pip install it before checking wrong interpreter / global mypy python -m mypy inside the venv first-party src/ not on path mypy_path / pip install -e .
Three distinct causes, three distinct remedies — diagnosing which one you have is the whole job.
# 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

For a src/ layout, tell mypy where the roots live rather than relying on discovery. Both mypy_path (config) and MYPYPATH (environment) prepend search roots:

# pyproject.toml — mypy 1.10+
[tool.mypy]
mypy_path = "src"
explicit_package_bases = true

explicit_package_bases matters for src/ layouts: it stops mypy inferring a package name from the wrong directory and mislabelling src/internal_billing as a top-level internal_billing. The most robust option, though, is an editable install (pip install -e .) so the package lands in site-packages exactly as consumers will import it — that keeps the runtime interpreter and mypy in agreement and sidesteps path juggling entirely. If you own the package, the proper long-term fix is to ship it as a real typed distribution with a py.typed marker rather than papering over it.

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 — you convert a loud, correct error into silent, unchecked code.

Runtime vs static analysis A module that raises 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, but with one important twist: the two do not share mypy’s default severity, which changes how they show up in CI.

  • reportMissingModuleSource — pyright found a stub (or types) but not the runtime source 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, so a default pyright run stays green while mypy fails the build.
  • reportMissingImports — pyright cannot resolve the import at all, the counterpart of mypy’s [import-not-found]. This is an error by default and will fail the run.
mypy versus pyright: codes and default severity A matrix pairing import-untyped with reportMissingModuleSource as a warning and import-not-found with reportMissingImports as an error. meaning mypy pyright (default) found, no types a types gap [import-untyped] error reportMissingModuleSource warning cannot resolve an env problem [import-not-found] error reportMissingImports error
The rows align one-to-one, but pyright downgrades the untyped case to a warning — the reason a green pyright run can hide what a red mypy run reports.

Configure the two independently in pyrightconfig.json (or the [tool.pyright] table in pyproject.toml). Each diagnostic accepts "none", "warning", or "error":

{
  "reportMissingModuleSource": "none",
  "reportMissingImports": "error",
  "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. Two behaviours differ from mypy and catch people out: pyright resolves against venvPath/venv (or the active environment) rather than a single interpreter, and its basic versus strict mode changes whether a bare untyped import is even reported. Because the severities differ out of the box, teams that run both checkers should raise reportMissingModuleSource to "error" to match mypy, or lower mypy’s strictness — otherwise the two tools disagree on whether the same untyped import fails CI.

There is a deeper reason the two disagree so often: useLibraryCodeForTypes, which pyright enables by default. When a package lacks a py.typed marker, pyright will still infer types by reading the library’s installed source code, whereas mypy refuses to look at un-marked annotations and reports [import-untyped]. So a dependency with annotations but no marker frequently type-checks cleanly under pyright and fails under mypy — not a bug in either tool, but a policy difference. Setting "useLibraryCodeForTypes": false makes pyright strict like mypy (it then raises reportMissingModuleSource instead of silently inferring), which is the setting to reach for when you want both checkers to agree that a missing marker is a real gap. The full mypy-vs-pyright picture is in pyright vs mypy comparison.

Common mistakes

Nearly every wrong turn here is the same shape: reaching for a broad Any-producing suppression when a narrow, correct fix exists. The contrast below pairs each anti-pattern with the fix it should have been.

Anti-pattern versus the correct fix Three rows contrast a global or misapplied suppression on the left with a scoped, environment-correct fix on the right. the tempting shortcut what it should be global ignore_missing_imports hides real broken imports [[tool.mypy.overrides]] per module only the named module → Any pip install types-* for a 404 import stub can't resolve a missing module fix the env, then re-check get the import working first mark own package as untyped throws away real annotations ship a py.typed marker expose the types you wrote
Every left-hand shortcut trades a precise error for silent Any; the right-hand fix keeps the checker honest.
  • 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. Run python -m mypy inside that venv, or set python_executable.
  • Reaching for # type: ignore on the import line. A bare # type: ignore hides whichever code fired, so an [import-not-found] masquerades as a handled [import-untyped]. If you must inline-suppress, name the code — # type: ignore[import-untyped] — so a later [import-not-found] on the same line still surfaces.
  • Marking a first-party package untyped instead of adding a py.typed marker. 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.

Back to Type Stubs and Third-Party Packages