On This Page
The Error
Traceback (most recent call last):
File "/home/dev/project/app.py", line 1, in <module>
import requests
ModuleNotFoundError: No module named 'requests'or, when it's your own package rather than a third-party one:
Traceback (most recent call last):
File "/home/dev/project/app.py", line 3, in <module>
from myapp.utils import helpers
ModuleNotFoundError: No module named 'myapp'and the variant that trips people up the most, because it looks like a broken install rather than a broken environment:
$ pip install requests
$ python app.py
Traceback (most recent call last):
File "app.py", line 1, in <module>
import requests
ModuleNotFoundError: No module named 'requests'ModuleNotFoundError is a subclass of ImportError, added in Python 3.6 (it replaced a plain ImportError for this case). The message format — No module named 'x' for a top-level package and No module named 'x.y' for a submodule — has been stable since 3.6 and is identical on 3.11, 3.12, 3.13, 3.14, and the current 3.15 pre-releases. What has changed across versions is the sibling error for from module import name: since Python 3.12, CPython suggests a close match when the name exists but is misspelled (ImportError: cannot import name 'chainmap' from 'collections'. Did you mean: 'ChainMap'?). There is no equivalent fuzzy suggestion for a missing top-level module name — ModuleNotFoundError gives you the bare message on every currently supported version, which is exactly why this error is so often misdiagnosed as "the package isn't installed" when the real problem is almost always which Python is running your code.
How to Reproduce It (step-by-step)
Reproduction A: two interpreters, one head-scratcher
bash
# System Python has nothing installed
$ python3 --version
Python 3.12.6
# Create a project venv
$ cd ~/project
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python -m pip install requests
Successfully installed requests-2.32.3 ...
# Now run the script with a *different* python on PATH
(.venv) $ deactivate
$ python3 app.py
Traceback (most recent call last):
File "app.py", line 1, in <module>
import requests
ModuleNotFoundError: No module named 'requests'requests was installed correctly — into .venv. The second python3 is the system interpreter, which has its own, separate site-packages. This exact pattern is what produces "works in the terminal but not in VS Code" or "works for me but not in Jupyter": the terminal has the venv activated, the IDE or notebook kernel is pointed at a different interpreter entirely.
Confirm it directly instead of guessing:
bash
$ which python
/home/dev/project/.venv/bin/python
$ python -c "import sys; print(sys.executable); print(sys.path)"
/usr/bin/python3
['', '/usr/lib/python3.12', '/usr/lib/python3.12/lib-dynload',
'/usr/lib/python3/dist-packages']If sys.executable isn't the interpreter inside .venv, that's the entire bug.
Reproduction B: your own package, run the wrong way
text
project/
├── myapp/
│ ├── __init__.py
│ └── utils.py
└── app.pypython
# app.py
from myapp.utils import helpersbash
$ cd project
$ python app.py
myapp imported finebash
$ cd project/myapp
$ python ../app.py
Traceback (most recent call last):
File "../app.py", line 1, in <module>
from myapp.utils import helpers
ModuleNotFoundError: No module named 'myapp'Running python app.py inserts the script's own directory at sys.path[0], not the directory you launched from. Change the working directory relative to the script and myapp is no longer a sibling on sys.path.
Reproduction C: pip itself goes missing inside a venv
bash
$ python3.12 -m venv .venv --without-pip
$ source .venv/bin/activate
(.venv) $ pip install requests
(.venv) $ python -m pip install requests
/home/dev/project/.venv/bin/python: No module named pip--without-pip is the deliberate repro here; the same message shows up after a corrupted venv, a python -m ensurepip failure, or a base interpreter upgrade that leaves an old venv pointing at a site-packages that no longer matches.
Version Behaviour Matrix (Python 3.11 / 3.12 / 3.13 / 3.14 / 3.15)
ModuleNotFoundError's own message text is version-neutral — it hasn't changed since Python 3.6. What varies across releases is the surrounding environment behavior that causes it:
| Version | Support status (as of Sep 2026) | Relevant change |
|---|---|---|
| 3.11 | Security-only (EOL ~Oct 2027) | -P / PYTHONSAFEPATH added (PEP not required, bpo-based) to stop the script/current directory from silently shadowing installed packages |
| 3.12 | Security-only (EOL ~Oct 2028) | distutils removed (PEP 632); venv no longer pre-installs setuptools/pip's build deps, so pkg_resources-based imports that worked on 3.11 now raise ModuleNotFoundError: No module named 'distutils' or 'pkg_resources' until you pip install setuptools; from x import y gained "Did you mean" suggestions |
| 3.13 | Bugfix, moving to security-only around its Oct 2026 anniversary | No change to import error text; free-threaded and JIT builds ship as experimental, unrelated to this error |
| 3.14 | Current bugfix release (released Oct 7, 2025) | concurrent.futures.ProcessPoolExecutor's default start method on Unix (other than macOS) changes from fork to forkserver; if a project relied on fork implicitly inheriting a module that was only ever imported in the parent process, a worker can now hit ModuleNotFoundError where it didn't before |
| 3.15 | Pre-release (final due Oct 1, 2026; rc2 on Sep 1, 2026) | Lazy imports are opt-in and UTF-8 mode is on by default; neither changes ModuleNotFoundError text, but lazy imports mean an import that used to fail at module load now fails later, at first use — worth knowing when you're debugging where the traceback originates |
Check devguide.python.org/versions before you rely on any of these dates — they get adjusted.
Why It Happens — Surface Level
Python didn't find a module by that name anywhere it looked. That's it — there's no ambiguity in the mechanism. The confusion is entirely about where it looked: every Python installation and every virtual environment has its own sys.path, and "I installed it" only helps if you installed it into the same sys.path that's about to run your import statement.
Why It Happens — Under the Hood
When you write import requests, the interpreter asks sys.meta_path — an ordered list of finder objects — to locate a ModuleSpec for the name "requests". The default finders check, in order: sys.modules (already imported?), built-in and frozen modules, then PathFinder, which walks sys.path looking for a matching directory or file. sys.path itself is assembled at startup from several sources with a specific order: an entry for the script's directory or the current directory (per the invocation-mode rules below), PYTHONPATH, the standard library, site-packages for the running interpreter (including any activated venv), and .pth files dropped into site-packages. If no finder produces a spec, import raises ModuleNotFoundError — there's no fallback, no search of "every Python on the machine," just this one process's sys.path.
That sys.path[0] entry is where invocation style matters and is the most under-diagnosed part of this error:
python script.py→sys.path[0]is the directory containingscript.pypython -m package.module→sys.path[0]is the current working directorypython -c "..."→sys.path[0]is the current working directory- an installed console-script entry point (e.g. a
pip-installed CLI) →sys.path[0]is that script's own install directory, not your project
Prove it to yourself instead of trusting a mental model:
bash
$ python -c "import sys; print(sys.path[0] or '<empty = cwd>')"
$ python -m pkg.mod # add the same print inside pkg/mod.py-P (3.11+) or PYTHONSAFEPATH=1 suppresses that automatic prepend entirely — useful for reproducing "it works on my machine but not in CI" bugs where a stray module in the current directory was silently shadowing the real one.
Separately, venv and virtualenv work by pointing a fresh interpreter's sys.prefix at a private site-packages directory and (on POSIX) symlinking or copying the interpreter binary. pip install inside an activated venv writes packages into that site-packages. If python, python3, and pip on your PATH resolve to a different binary than the one the venv created — a very common outcome of IDEs remembering a stale interpreter path, sudo pip install writing to the system location, or a notebook kernel registered against a Python that predates your current venv — the install and the import are simply talking to two different filesystems that happen to share a name.
You can inspect all of this directly:
bash
$ python -m sysconfig | grep -E "prefix|purelib"
$ python -c "import importlib.util as u; print(u.find_spec('requests'))"
$ python -c "import requests" ; echo $?importlib.util.find_spec returns None (not an exception) when a module can't be located, which is a clean way to check "is this importable from this interpreter" without triggering a full traceback in diagnostic tooling.
The Fix
Quick fix — confirm and align the interpreter:
bash
# Before: guessing
$ pip install requests
$ python app.py # ModuleNotFoundError anyway
# After: verify which python pip is targeting, every time
$ python -m pip install requests
$ python app.pypython -m pip install ... is the fix, not a stylistic preference: it guarantees pip runs as a module of this exact interpreter, instead of resolving a possibly-different pip executable from PATH.
In an IDE or notebook, the fix is almost always a selector, not code:
- VS Code:
Python: Select Interpreter(bottom-right status bar or the command palette), point it at.venv/bin/python. - PyCharm: Settings → Project → Python Interpreter → point it at the same venv.
- Jupyter: the kernel is a separate concept from your shell's active venv. Register the venv explicitly:
bash
(.venv) $ python -m pip install ipykernel
(.venv) $ python -m ipykernel install --user --name project-venvthen pick project-venv from the kernel menu — picking the venv in a terminal does nothing for a notebook kernel that was registered against a different interpreter months ago.
For the "my own package" case (Reproduction B):
python
# Before: relies on cwd being the project root
$ python app.py # only works from project/
# After: run as a module from the project root, which fixes sys.path[0]
$ python -m app # if app.py is app/__main__.py or similar
# or, simplest and most robust: install the project so imports don't
# depend on cwd at all
$ python -m pip install -e .
$ python app.py # now works from any directoryFor the missing-pip-in-a-venv case (Reproduction C):
bash
(.venv) $ python -m ensurepip --upgrade
(.venv) $ python -m pip install --upgrade pipFor 3.12+ distutils/pkg_resources breakage:
bash
(.venv) $ python -m pip install setuptoolsonly if you genuinely still depend on it — the better fix is removing the pkg_resources import in favor of importlib.metadata (stdlib since 3.8).
Best Practices & The Better Design
The recurring theme in every variant above is implicit interpreter and path resolution. Replace each implicit dependency with an explicit one:
- Use a
src/layout and an installed (even if editable) package instead of relying onsys.path[0]orPYTHONPATHtricks.python -m pip install -e .with apyproject.tomlmakes your package importable from anywhere, regardless of current directory.
toml
# pyproject.toml
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"project/
├── pyproject.toml
├── src/
│ └── myapp/
│ ├── __init__.py
│ └── utils.py
└── tests/- Run modules with
python -m pkg.modor through a console-script entry point defined inpyproject.toml, not by path — it removes the "which directory was I in" variable entirely. - One virtual environment per project, created and activated the same way every time (
uv venv, orpython -m venv .venv), with a committed lockfile (uv.lock, or a hash-pinnedrequirements.txt) so "it's installed" means the same thing on every machine and in CI. - Never
sudo pip installinto system Python for a project dependency — it's how "works for me" environments happen, and it's the direct cause of a large share ofPermissionError-flavored variants of this same bug. - Prefer
uv runor an activated venv'spython -minvocations over barepython/piponPATH, sincePATHresolution is exactly the ambiguity that causes Reproduction A.
Prevent It in the Long-Term
- Add
pip check(oruv pip check/uv sync --frozenin CI) as a cheap sanity gate — it catches an environment that's silently missing a dependency before your test suite does it the expensive way. - Run
python -c "import sys; print(sys.executable)"(or log it) at the top of CI jobs and deploy scripts — an interpreter mismatch shows up in one line instead of a confusing traceback three steps later. - Ruff's
TID252(relative import) andINP001(implicit namespace package, missing__init__.py) catch layout problems that eventually surface as import errors. - In CI, test against an environment built the same way production is built — from the committed lockfile, in a matching container image — rather than "whatever's already installed on the runner."
- For notebooks, standardize on
python -m ipykernel installas an onboarding step so kernels are never silently stale relative to the project venv. - Add a one-line smoke test (
python -c "import myapp") as the very first CI step; it fails fast and points straight at the environment instead of getting buried in a 40-test failure log. - Document, in the README, the exact three commands that create the environment (
python -m venv, activate,python -m pip install -e .) — most recurring instances of this error on a team come from everyone having a slightly different personal ritual for "setting up the project."
This error connects directly to two others worth reading next: ImportError: attempted relative import with no known parent package (the python -m vs python file.py distinction goes deeper there) and error: externally-managed-environment (PEP 668), which is what happens when you try to pip install outside a venv at all on modern Debian/Ubuntu and Homebrew Python.
Important
ModuleNotFoundErroralmost never means "the package doesn't exist" — it means this specific interpreter can't see it on itssys.path.python -c "import sys; print(sys.executable)"is the single most useful diagnostic line for this error; run it before you reinstall anything.python -m pip install Xties the install to the interpreter that will run your code; a barepip install XtrustsPATHto resolve to the same one, and it often doesn't.- IDE and notebook kernels hold their own interpreter selection, independent of your shell's activated venv — check both.
- A
src/layout with an editable install (pip install -e .) eliminates cwd-dependent imports permanently, instead of patchingsys.pathby hand each time it breaks.
CODELZ Newsletter
Join the newsletter to receive the latest updates in your inbox.