ModuleNotFoundError Even Though You Definitely Installed It
pip says it is installed, python says it is not. Both are telling the truth about different Pythons.
The short answer
ModuleNotFoundError: No module named 'requests'
You installed it into a different Python than the one running your code. Find out which:
which python; which pip
python -c "import sys; print(sys.executable)"
python -m pip --version
If pip --version reports a different path from sys.executable, that is your bug. Install with python -m pip install requests instead of pip install requests, which guarantees the same interpreter.
Tested on Python 3.13.2.
Why this happens so often
Most systems have several Pythons. A macOS machine can easily have the system Python, a Homebrew Python, a pyenv Python, one per virtualenv, and one inside a Conda environment.
Each has its own site-packages. pip is a script with a shebang pointing at whichever interpreter it was installed for, and python is whatever your PATH resolves first. Nothing keeps them in sync.
The reliable fix is to stop invoking pip directly:
python -m pip install requests
python -m pip runs pip using the interpreter you just named, so the install always lands where the import will look. I would make this a habit even when things are working.
Diagnosing it properly
python -c "import sys; print('\n'.join(sys.path))"
That is the exact list of directories Python searches, in order. If your package's site-packages is not in it, the import cannot succeed no matter what pip reported.
To see where a package actually landed:
python -m pip show requests | grep Location
Compare that against the sys.path output. If it is absent, you have confirmed the diagnosis.
For the venv case specifically:
echo $VIRTUAL_ENV
python -c "import sys; print(sys.prefix != sys.base_prefix)"
True means you are inside a virtual environment. False while $VIRTUAL_ENV is set means you activated a venv and then something changed PATH, which happens with some shell prompts and with sudo.
The specific traps
sudo pip
sudo pip install requests # installs to the system Python, as root
python app.py # runs your venv Python
sudo resets the environment, so your venv is not active inside it. The package installs somewhere your code will never look, and you now also have root owned files in a system directory.
There is essentially never a good reason to sudo pip install. If you feel the need, use a venv or pipx.
The venv was created with a different Python
python3.11 -m venv .venv
source .venv/bin/activate
python --version # 3.11, permanently
A venv is bound to the interpreter that created it. Upgrading your system Python does not upgrade the venv, and a venv created against a Python you later uninstalled will break with confusing errors about missing shared libraries.
Recreate rather than repair:
rm -rf .venv && python3.13 -m venv .venv && source .venv/bin/activate
python -m pip install -r requirements.txt
Your file shadows the package
myproject/
requests.py <-- this
app.py
import requests finds your file first, because the script's directory is at the front of sys.path. The error is often not ModuleNotFoundError but something stranger inside the module, like AttributeError: module 'requests' has no attribute 'get', which is a good tell.
Common offenders: email.py, types.py, json.py, logging.py, queue.py, test.py, select.py, string.py.
A stray __pycache__ from a file you already deleted can also do this. Clear it:
find . -name "__pycache__" -type d -exec rm -rf {} +
Editable install without the package installed
ModuleNotFoundError: No module named 'myapp'
Your own package, not a dependency. Running python src/myapp/main.py directly puts src/myapp on the path, not src, so from myapp.utils import x fails.
Install your project in editable mode:
python -m pip install -e .
Then imports resolve consistently regardless of where you run from. This requires a pyproject.toml, which you want anyway.
Alternatively run as a module, which puts the current directory on the path:
python -m myapp.main
Note that PYTHONPATH=. python app.py also works and is the version I would avoid, because it works on your machine and not in the container.
Different Python in the container
Locally fine, in Docker ModuleNotFoundError. Usually one of:
# installs for root, runs as appuser who cannot see it
RUN pip install -r requirements.txt
USER appuser
# correct
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
USER appuser
Or a multi stage build that copies the application without copying the installed packages:
FROM python:3.13-slim AS build
RUN pip install --user -r requirements.txt
FROM python:3.13-slim
COPY --from=build /root/.local /root/.local # this line is easy to forget
ENV PATH=/root/.local/bin:$PATH
This is one of the causes of a build that works locally and fails in CI, and it usually presents at runtime rather than build time, which makes it slower to connect.
Namespace package collisions
Two distributions installing into the same top level name, or a package installed both from PyPI and from a local checkout. Check for duplicates:
python -c "import mypkg; print(mypkg.__file__)"
python -m pip list | grep -i mypkg
If __file__ points somewhere unexpected, you have two copies and the wrong one is winning.
The modern answer
Most of this category disappears if you stop managing environments by hand.
uv has become my default. It creates the environment, resolves, installs, and runs, all bound to one interpreter:
uv venv
uv pip install -r requirements.txt
uv run python app.py
uv run guarantees the interpreter and the packages match, which removes the entire class of error this post is about.
Poetry and PDM do the same thing with more project management around it. pipx is the right tool for installing command line applications, so they get isolated environments instead of polluting your system Python.
Whichever you pick, the property that matters is that one command owns both the environment and the execution. The failure mode in this post exists because pip and python are two independent commands that can disagree.
A diagnostic script
Worth keeping around, because it answers every question above at once:
import sys, site, os
print("executable :", sys.executable)
print("version :", sys.version.split()[0])
print("in venv :", sys.prefix != sys.base_prefix)
print("VIRTUAL_ENV:", os.environ.get("VIRTUAL_ENV"))
print("site-packages:")
for p in site.getsitepackages():
print(" ", p)
print("sys.path:")
for p in sys.path:
print(" ", p or "(cwd)")
Run it with the same command that runs your application, not from your shell. If your app starts through gunicorn, a systemd unit, or an entrypoint script, run it there, because the environment those see is frequently not the environment you see.
That last point is the general lesson. This error is almost never about the package. It is about which interpreter is running and what it can see, and the fastest path to an answer is always to make the program tell you rather than to reason about what it should be.