Type Stubs and Third-Party Packages
When you import a library, your type checker has to find annotations for it from somewhere. There are four places it looks, in a fixed order, and knowing that order is the difference between fixing a [import-untyped] error in thirty seconds and disabling type checking for a whole subtree by accident.
A dependency can ship its own annotations inline (marked with a py.typed file per PEP 561), rely on a separate stub-only distribution such as types-requests, be covered by the typeshed collection that mypy bundles, or ship nothing at all. This cluster explains that resolution chain, the exact error codes each checker raises when the chain comes up empty, and how to plug the gaps yourself.
Any.Inline types shipped with the package
The modern, preferred source is annotations that live inside the library itself. A maintainer annotates their .py files and drops an empty marker file named py.typed into the package directory, then declares it as package data so it lands in the wheel. That marker is the PEP 561 signal that says “trust the inline annotations here.” Without it, checkers ignore the annotations even if they are present, because an un-marked package is assumed to be accidentally-annotated rather than deliberately typed.
Libraries such as attrs, pydantic, and httpx all ship a py.typed marker today. When you author your own distributable package, this is the route you want — the types version with the code and never drift. The mechanics of adding the marker and wiring it into your build are covered in shipping inline types with py.typed.
Separate stub (.pyi) packages
Many older or C-extension libraries have no inline types. For these, the community publishes stub-only distributions: packages containing just .pyi files that describe the API. The naming convention is types-<name>, so requests is typed by types-requests, PyYAML by types-PyYAML, and so on. These live mostly in the typeshed stubs/ tree and are auto-published to PyPI.
$ pip install types-requests
$ mypy app.py
Success: no issues found in 1 source file
A stub package installs alongside the real library and the checker prefers it over the bundled copy. Because stubs and library ship separately, they can fall out of sync — pin the stub version close to the library version in your lockfile.
# Python 3.12+, checked with mypy 1.10 / pyright 1.1.370
import requests # types-requests supplies the .pyi stubs
def fetch_status(url: str) -> int:
resp = requests.get(url, timeout=5)
return resp.status_code # resp: requests.Response — fully typed
Bundled typeshed
Both mypy and pyright vendor a snapshot of typeshed, the central repository of stubs for the standard library and thousands of third-party packages. Standard-library types (os, json, pathlib) always come from here — you never install a stub for the stdlib. For third-party modules, the bundled copy is a fallback that a freshly-published types-* package or newer inline types will override.
py.typed markers are invisible at runtime. Python never reads a .pyi file when it executes; only the checker does. A stub can therefore describe a signature the real module does not actually honor — if a stub is wrong, mypy passes but the program still raises TypeError. Treat mismatched stubs as bugs to report upstream, not as ground truth.
When nothing is found: the error codes
If none of the four sources supplies types, each checker reacts differently, and the two mypy codes mean genuinely different things:
- mypy
[import-untyped]— the module was found on disk but ships no types and has no stub package. mypy tells you exactly whichtypes-*package to install when one exists. - mypy
[import-not-found]— the module itself could not be imported at all (wrong virtualenv, missing dependency, badMYPYPATH). This is an environment problem, not a typing gap. - pyright
reportMissingModuleSource— pyright found a stub but not the runtime module (or vice versa); usually harmless, often a warning. - pyright
reportMissingImports— pyright could not resolve the import at all, the analogue of mypy’s[import-not-found].
# Python 3.12+, checked with mypy 1.10
import orderlib # error: Skipping analyzing "orderlib": module is
# installed, but missing library stubs [import-untyped]
import notinstalled # error: Cannot find implementation or library
# stub for module "notinstalled" [import-not-found]
Telling these apart is the whole battle — fixing import-untyped and missing imports walks through the resolution for each.
Generating your own stubs
When a dependency has no inline types, no types-* package, and no typeshed entry, you can synthesize stubs. mypy ships stubgen, which introspects a module and emits .pyi skeletons full of Any that you then refine by hand. You point the checker at the result with MYPYPATH / mypy_path (mypy) or stubPath (pyright). See generating stub files with stubgen for the full workflow.
Common mistakes
- Adding annotations but forgetting
py.typed. Downstream checkers still report mypy[import-untyped]because the PEP 561 marker is what flips the switch — the annotations alone do nothing. - Silencing
[import-not-found]withignore_missing_imports. That flag hides a real import failure. The module is genuinely unresolvable; fix the environment instead of masking it, or you will shipAny-typed code. - Installing a
types-*package but not pinning it. A stub package that drifts ahead of the library produces phantom[attr-defined]errors for methods your installed version does not have. - Assuming the stdlib needs stubs. Standard-library types come from bundled typeshed automatically; installing anything for
osorjsonis a sign of a misconfigured environment.
FAQ
What is the difference between [import-untyped] and [import-not-found]?
[import-untyped] means mypy imported the module but it has no type information — a typing gap you close with a stub package or a scoped ignore. [import-not-found] means mypy could not import the module at all — an environment or path problem you fix by installing the dependency or correcting MYPYPATH.
Do I need to install stubs for the standard library?
No. Both mypy and pyright bundle typeshed, which includes complete stubs for every standard-library module. You only install types-* packages for third-party libraries that lack inline types.
Which source wins when a library ships py.typed and a types-* package also exists?
Inline py.typed types take precedence over an installed stub package, which in turn takes precedence over bundled typeshed. The first source in the chain that supplies types is the one used.