Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Python Module Not Found: A Diagnostic Sequence That Actually Fixes It

Sean

Platform Writer

Jun 19, 2026
7 min read

The ModuleNotFoundError happens when the Python interpreter that is running the script is not the Python interpreter that has the module installed. The four causes, in order of frequency: the script is using python3 and the module is installed in python, the script is using the system Python and the module is in a virtual environment, the module was installed with pip from a different Python than the one running the script, or the module name does not match the package name on PyPI. Every “Python module not found” report is one of those four. The diagnostic takes 60 seconds once you know which one it is.

The reason this is a top search is that the error is genuinely confusing. Python is installed, the module is installed, and yet the import fails. The list of reasons is short, but the surface looks infinite because every developer hits a different one of the four. This post is the diagnostic that catches all of them.

Python module not found: a diagnostic sequence that actually fixes it

Table of contents

The direct answer: which of the four is it

Run these four commands in order. The answer is in the first one that disagrees with itself.

# 1. Which Python is running the script?
which python3
python3 -c "import sys; print(sys.executable)"

# 2. Which pip is in the path?
which pip
pip --version

# 3. Where is the module installed?
python3 -m pip show <module-name>

# 4. Can Python find it on sys.path?
python3 -c "import sys; print('\n'.join(sys.path))"

If the output of (1) and (2) point to different directories, you are using two different Pythons. If the output of (3) says “not found” but pip list shows the module, you are using two different Pythons. If the output of (3) shows the path and (4) does not include that path, the install is in a different prefix.

In 90% of cases, the fix is to make (1) and (2) the same Python.

Cause 1: the wrong Python is running the script

The most common cause. The system has Python 2, Python 3.10, Python 3.12, and a Homebrew Python 3.13. The user ran pip install requests, which bound to the Homebrew Python. The script runs with /usr/bin/python3, which is the system Python 3.10, which does not have requests.

The diagnostic:

$ which python3
/usr/bin/python3
$ which pip
/home/me/.local/bin/pip
$ head -1 /home/me/.local/bin/pip
#!/home/me/.local/share/uv/python/cpython-3.12.7-linux-x86_64-gnu/bin/python3

The shebang in the pip script points to one Python. The shebang in python3 (or the symlink) points to another. The fix is to use the same Python for both:

# Use the Python that pip belongs to
/home/me/.local/share/uv/python/cpython-3.12.7-linux-x86_64-gnu/bin/python3 -m pip install requests

# Or, more usefully, always use `python3 -m pip`, never bare `pip`
python3 -m pip install requests

The python3 -m pip form is the single most important habit for avoiding this class of bug. The bare pip is whatever Python happened to be first on $PATH when the alias or wrapper was installed. The python3 -m pip form is the pip that belongs to the python3 you are about to run the script with.

Cause 2: the script is outside the virtual environment

The second most common cause. The user created a virtual environment, activated it, installed the module, then ran the script from a different shell or after deactivating the venv. The script runs with the system Python. The module is in the venv.

The diagnostic:

# Are you in the venv?
echo $VIRTUAL_ENV

# What does sys.prefix say?
python3 -c "import sys; print(sys.prefix)"

# Is the module on the path?
python3 -c "import requests; print(requests.__file__)"

If $VIRTUAL_ENV is empty but you expected it to be set, you are not in the venv. If sys.prefix is /usr but you expected it to be the venv path, you are not in the venv. The fix is to activate the venv (or use its Python directly):

# Option 1: activate
source .venv/bin/activate
python my_script.py

# Option 2: use the venv's Python directly, no activation needed
.venv/bin/python my_script.py

# Option 3: install with --user and accept the global install
python3 -m pip install --user requests

For a deployment scenario, the venv is the answer: the Dockerfile creates a venv, installs the requirements into it, and runs the script with the venv’s Python. The venv is the only reason python3 and pip agree on which Python they belong to.

Cause 3: pip is bound to a different Python than the script uses

A specific variant of Cause 1, common on macOS with Homebrew. Homebrew installs Python to a versioned directory, and the system Python is at /usr/bin/python3. The user did brew install python, got python3.13, and ran pip3 install requests. The pip3 was bound to the Homebrew Python. The script runs with the system Python.

The diagnostic:

$ pip3 --version
pip 24.0 from /opt/homebrew/lib/python3.13/site-packages/pip (python 3.13)
$ /usr/bin/python3 --version
Python 3.9.6

The versions do not match. The fix is the same as Cause 1: use python3 -m pip and make sure python3 is the one you think it is.

The deeper fix on macOS is to set the PATH so the Homebrew Python is first, and to use python3 consistently. The trap on Apple Silicon is that Homebrew installs to /opt/homebrew, not /usr/local. The trap on Intel Macs is the opposite. Pick the convention, set the PATH, and stick to it.

Cause 4: the module name does not match the package name

The fourth cause, and the most surprising. The package on PyPI is python-dotenv, but the module is dotenv. The package is PyYAML, but the module is yaml. The package is python-dateutil, but the module is dateutil. The install succeeds, but import python-dotenv fails.

The diagnostic:

$ pip show python-dotenv
Name: python-dotenv
...
$ python3 -c "import dotenv"     # works
$ python3 -c "import python-dotenv"   # fails

The fix is to import the module name, not the package name. The convention is “package name is what you pip install, module name is what you import.” Some packages follow it (Django, pip install django, import django). Some do not (Pillow, pip install pillow, import PIL).

For ambiguous cases, check the package’s documentation or look at the top_level.txt file inside the installed package directory. The list of common offenders is short and worth memorizing: python-dotenvdotenv, PyYAMLyaml, python-dateutildateutil, PillowPIL, scikit-imageskimage, python-crontabcrontab.

The two-minute diagnostic that catches all four

For any “module not found” report, the script:

#!/usr/bin/env bash
# debug-module.sh — find out which Python is missing the module

MODULE="${1:?usage: debug-module.sh <module-name>}"

echo "=== which python3 ==="
which python3 || echo "(not found)"

echo
echo "=== which pip ==="
which pip || echo "(not found)"

echo
echo "=== python3 -m pip --version ==="
python3 -m pip --version

echo
echo "=== pip show $MODULE ==="
python3 -m pip show "$MODULE" || echo "(not installed for this python3)"

echo
echo "=== python3 -c 'import $MODULE' ==="
python3 -c "import $MODULE; print('$MODULE imported from', $MODULE.__file__)" \
  || echo "(import failed)"

echo
echo "=== sys.path ==="
python3 -c "import sys; print('\n'.join(sys.path))"

echo
echo "=== sys.prefix / sys.executable ==="
python3 -c "import sys; print('prefix:', sys.prefix); print('exec:', sys.executable)"

echo
echo "=== VIRTUAL_ENV ==="
echo "${VIRTUAL_ENV:-(not set)}"

Run with the module name as the argument. The output shows which Python is running, which Python pip is bound to, where the module is installed, and whether sys.path includes that path. The discrepancy is the bug.

The script is the diagnostic the rest of this post is a manual version of. Save it as debug-module.sh in your ~/bin, alias it, and run it on every “module not found” report. The fix is in the first place the output disagrees with itself.

The deployment-time case: it works locally, breaks in the container

The fourth cause is actually the most common in deployment scenarios: the script works locally because the developer’s local Python is one version, and the container has a different Python that does not have the module.

The diagnostic in the container:

docker run --rm myimage python3 -m pip list
docker run --rm myimage python3 -c "import requests"

If the second command fails, the container’s Python does not have requests. The fix is in the Dockerfile: the pip install step and the CMD step have to use the same Python, and the requirements have to be installed before the script runs.

The standard shape:

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

CMD ["python3", "my_script.py"]

The pip install runs at image build time, in the same Python the CMD uses, and the --no-cache-dir keeps the image small. For a hosted platform that handles the Dockerfile for you, the same logic applies: the requirements have to be installed in the same runtime that runs the code. The MCP server build flow follows the same pattern for Python MCP servers: pin the version, install the requirements, run the same Python you installed them with.

How this fits the rest of the stack

A working Python install is also a hosting cost — the runtime version, the requirements size, the build minutes, the bandwidth for the package index, and the storage for the virtual environment each show up as a line item. The team’s mental model for the Python deploy cost is the sum of those numbers, and the team should know the total before the requirements file grows. The RunxBuild hosting calculator is the right place to model that — pick the Python runtime size, the build frequency, the storage, and the bandwidth, and the calculator shows what the deploy costs at the team’s actual usage.

Useful related references:

FAQ

Why does Python say “No module named X” when I just installed it?

The most common reason is that pip and python3 are bound to different Python interpreters. Use python3 -m pip install X instead of bare pip install X. The -m flag ensures pip installs into the same Python that the next python3 invocation will use.

How do I see where a module is installed?

python3 -m pip show <module-name> shows the package location. python3 -c "import <module>; print(<module>.__file__)" shows the path Python actually loaded it from. If the two paths disagree, you have two different Pythons.

How do I install a module for a specific Python version?

Use that Python’s -m pip: python3.12 -m pip install requests installs requests for the Python 3.12 interpreter. The bare pip command is whichever Python’s pip was first on $PATH, which is rarely the one you want.

Should I use a virtual environment?

Yes, always for any project. A virtual environment (python3 -m venv .venv) gives you an isolated Python with its own site-packages, and the pip and python3 inside the venv are guaranteed to agree.

Why does import python-dotenv fail but import dotenv works?

Because the PyPI package is python-dotenv (with hyphens) but the Python module is dotenv (no hyphen). The convention is that you pip install the package name and import the module name. Some packages have different conventions; check the docs.

How do I fix “module not found” in a Docker container?

Make sure the pip install and the CMD use the same Python, and that the install happens at build time (not at run time). The standard shape is RUN pip install -r requirements.txt followed by CMD ["python3", "my_script.py"].

#python module not found#modulenotfounderror#python import error#pip install wrong python#python venv path#python sys.path