Skip to content

Python ModuleNotFoundError: Causes and Fixes

ModuleNotFoundError: No module named 'x' almost always means the wrong interpreter, not a missing package. Here's how to diagnose and fix it for good.

Python python-errors ModuleNotFoundError imports sys-path virtualenv pip packaging
Bharath G
Reading Progress

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.py

python

# app.py
from myapp.utils import helpers

bash

$ cd project
$ python app.py
myapp imported fine

bash

$ 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:

VersionSupport status (as of Sep 2026)Relevant change
3.11Security-only (EOL ~Oct 2027)-P / PYTHONSAFEPATH added (PEP not required, bpo-based) to stop the script/current directory from silently shadowing installed packages
3.12Security-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.13Bugfix, moving to security-only around its Oct 2026 anniversaryNo change to import error text; free-threaded and JIT builds ship as experimental, unrelated to this error
3.14Current 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.15Pre-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.pysys.path[0] is the directory containing script.py
  • python -m package.modulesys.path[0] is the current working directory
  • python -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.py

python -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-venv

then 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 directory

For the missing-pip-in-a-venv case (Reproduction C):

bash

(.venv) $ python -m ensurepip --upgrade
(.venv) $ python -m pip install --upgrade pip

For 3.12+ distutils/pkg_resources breakage:

bash

(.venv) $ python -m pip install setuptools

only 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 on sys.path[0] or PYTHONPATH tricks. python -m pip install -e . with a pyproject.toml makes 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.mod or through a console-script entry point defined in pyproject.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, or python -m venv .venv), with a committed lockfile (uv.lock, or a hash-pinned requirements.txt) so "it's installed" means the same thing on every machine and in CI.
  • Never sudo pip install into system Python for a project dependency — it's how "works for me" environments happen, and it's the direct cause of a large share of PermissionError-flavored variants of this same bug.
  • Prefer uv run or an activated venv's python -m invocations over bare python/pip on PATH, since PATH resolution is exactly the ambiguity that causes Reproduction A.

Prevent It in the Long-Term

  • Add pip check (or uv pip check / uv sync --frozen in 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) and INP001 (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 install as 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

  • ModuleNotFoundError almost never means "the package doesn't exist" — it means this specific interpreter can't see it on its sys.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 X ties the install to the interpreter that will run your code; a bare pip install X trusts PATH to 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 patching sys.path by hand each time it breaks.
Pythonpython-errorsModuleNotFoundErrorimportssys-pathvirtualenvpippackaging

From aspiring developer to blogger, I test learning platforms, simplify programming syntax, and share resources that work. Helping you code smarter as I grow myself. New or experienced, you're welcome here.

Comments