technology · PRACTICAL GUIDE
pip says installed, but ModuleNotFoundError? Prove the interpreter and import path
Use the failing process to identify its Python, verify that interpreter's distribution metadata, map the real import name, explain sys.path, and close only on a clean import.

A successful pip install and a failed import can both be true when they describe different interpreters, different names or different search paths.
Start inside the failing process. Bind every later command to its absolute Python executable, then prove metadata, name mapping, path resolution and module origin before changing the environment.
Capture the failing runtime before changing anything
Run the exact failing command again and save the command, working directory and full traceback. Then, through that same launch path, print sys.executable, sys.version, sys.prefix, sys.base_prefix and sys.path. A terminal, IDE, notebook kernel, service and shebang can all select different interpreters even when their prompt looks similar.
Do not start with sudo pip, --user, a global PYTHONPATH change or a copied site-packages path. Those actions add another state without proving which interpreter executed the file. The first useful artifact is the absolute executable path reported by the process that failed.
Ask that interpreter whether the distribution is installed
Use the captured executable explicitly: /absolute/python -m pip show <distribution>. Record Name, Version and Location, then use --files when you need to confirm installed paths. A successful bare pip show is irrelevant if the pip launcher belongs to another interpreter.
If this exact interpreter reports no distribution, install into the intended virtual environment using that interpreter's -m pip after reviewing the project's lock or requirements. If it reports a Location outside every entry in the captured sys.path, diagnose environment activation, user-site policy, pyvenv.cfg, service configuration or container boundaries instead of reinstalling blindly.
Separate the distribution name from the import name
The name passed to pip is a distribution name; the name after import is an import package. The Python Packaging User Guide states that they do not have to match. Pillow imports as PIL, and one distribution may expose several top-level packages while a namespace may be shared by several distributions.
Confirm the project's documented import, inspect the installed files, and when useful query importlib.metadata.packages_distributions() from the failing interpreter. Do not conclude that installation failed only because import <distribution-name> fails; first establish the correct import name and whether the distribution actually provides it.
Explain the search path instead of patching it
Python constructs sys.path from the script or current directory, environment variables, executable landmarks, virtual-environment configuration and site processing. Compare that documented model with the captured runtime. Use python -m site to record USER_SITE and ENABLE_USER_SITE when user installs are involved.
A service may disable or replace user-site behavior; an IDE or notebook may retain an old kernel; a script shebang may name another executable. Repair the smallest launcher or environment boundary. Avoid permanent global PYTHONPATH and inline sys.path.append fixes unless the application intentionally owns that package layout and tests it.
Rule out shadowing, then require a clean exit proof
From the project directory, check for files or folders named like the target module and for partially initialized packages. Use importlib.util.find_spec(<import-name>) before importing; after a successful import, print module.__file__ when present. A local requests.py or stale directory can win earlier on sys.path than site-packages.
The incident is closed only when the same executable that failed now reports the intended distribution Location, resolves the documented import from the intended path, imports in a clean process and runs the original script or test. Save executable, Location, sys.path or site output, find_spec result and module file as the proof. Reinstallation alone is not an exit criterion.
