/개발/pip install succeeded, but ModuleNotFoundError? Six-way interpreter path diagnosis
Developmentpythonpip

pip install succeeded, but ModuleNotFoundError? Six-way interpreter path diagnosis

pip install succeeded but import still fails? Instead of reinstalling, compare the running interpreter and pip install paths with four commands, pin the cause to one of six branches, and apply one-line fixes plus recurrence prevention for v

pip install succeeded, but ModuleNotFoundError? Six-way interpreter path diagnosis

It installed successfully — so why is it missing? The problem is coordinates, not the error

The terminal clearly printed this:

TEXT
Successfully installed requests-2.32.3

But on the very next line, import requests fails. The most common reaction is “maybe the install didn’t work,” so you run pip install again; next comes deleting the virtualenv and recreating it. If you’re lucky it gets fixed; if not, you’re back in the same place 30 minutes later. Reinstalling didn’t remove the cause — it only hid it.

In practice, this symptom almost always converges on a single cause:

The site-packages directory where pip wrote the files and the sys.path that python actually reads do not overlap.

In other words, the install really did succeed. It just succeeded for a different Python. So this article does not classify error messages. Error text does not tell you the cause; several causes converge on the same wording. Instead we take two coordinates and check whether they overlap.

  • Coordinate A: Which interpreter is actually running this code (sys.executable)
  • Coordinate B: Which interpreter is the pip you just used bound to (python -m pip -V, pip -V)

With just these two coordinates, the cause is pinned to one of six branches. For reference, parts 1–7 of the “Python Development Guide” series were about solving individual errors; this part 8 is the environment diagnosis runbook one layer above them. Cases where installation itself is blocked by policy (externally managed environments) or where downloads fail due to certificate problems were already covered in earlier parts — check those.

Scope: CPython 3.9–3.13, macOS (Homebrew/CLT), Ubuntu 22.04·24.04, Windows 10/11 (python.org installer), Docker, Jupyter/VS Code.

30-second coordinate capture: four commands and a decision table

Paste the following four commands in order, as-is. There is no prompt symbol.

Bash
which -a python python3 pip pip3
Bash
python -c "import sys; print(sys.executable); print('---'); print('\n'.join(sys.path))"
Bash
python -m pip -V
pip -V
Bash
python -c "import sysconfig; print(sysconfig.get_paths()['purelib'])"

1) which -a — list every candidate on PATH

Healthy output example (venv activated; every executable lives in the same venv)

TEXT
/home/dev/proj/.venv/bin/python
/usr/bin/python3
/home/dev/proj/.venv/bin/python3
/usr/bin/python3
/home/dev/proj/.venv/bin/pip
/home/dev/proj/.venv/bin/pip3

The first line is what matters. which -a lists in PATH priority order, so the top entry is what actually runs. If python’s first line and pip’s first line are in the same directory, you’re healthy.

Unhealthy output example

TEXT
/home/dev/proj/.venv/bin/python
/usr/bin/python3
/usr/local/bin/pip
/home/dev/proj/.venv/bin/pip

python is in .venv/bin, but pip is picked up from /usr/local/bin first. The install goes to /usr/local while execution happens in the venv. Classic mismatch.

2) sys.executable + sys.path — running interpreter and search path

Healthy output example

TEXT
/home/dev/proj/.venv/bin/python
---

/usr/lib/python3.12
/usr/lib/python3.12/lib-dynload
/home/dev/proj/.venv/lib/python3.12/site-packages

If the last line contains the venv’s site-packages, you’re healthy.

Unhealthy output example

TEXT
/usr/bin/python3
---

/usr/lib/python3.12
/usr/lib/python3.12/lib-dynload
/usr/lib/python3/dist-packages

You thought the venv was activated, but sys.executable is /usr/bin/python3 and .venv appears nowhere on sys.path. At this point the cause is already half confirmed.

3) python -m pip -V vs pip -V — pip’s self-report, twice

Healthy output example

TEXT
pip 24.2 from /home/dev/proj/.venv/lib/python3.12/site-packages/pip (python 3.12)
pip 24.2 from /home/dev/proj/.venv/lib/python3.12/site-packages/pip (python 3.12)

The two lines are identical.

Unhealthy output example

TEXT
pip 24.2 from /home/dev/proj/.venv/lib/python3.12/site-packages/pip (python 3.12)
pip 23.0 from /usr/lib/python3/dist-packages/pip (python 3.11)

Typing pip runs the system pip. In that state, pip install sends packages to /usr/lib/python3/dist-packages.

4) Confirm the install destination (bonus)

Bash
python -c "import sysconfig; print(sysconfig.get_paths()['purelib'])"

Healthy: /home/dev/proj/.venv/lib/python3.12/site-packages Unhealthy: /usr/lib/python3.12/site-packages (you thought the venv was on, but this is the system path)

To see where an already-installed package lives:

Bash
python -m pip show requests | grep -i location

Healthy: Location: /home/dev/proj/.venv/lib/python3.12/site-packages Unhealthy: Location: /home/dev/.local/lib/python3.12/site-packages (leaked into the --user area)

Decision table from two lines

The combination of sys.executable (A), the parentheses/path from python -m pip -V (B), and pip -V (C). By definition B always belongs to A, so the real variables are “do A and C match?” and “is A the interpreter I intended?”

Is A (sys.executable) the intended environment?Does C (pip -V) match A?VerdictNext action
YesYesEnvironment is consistent. The problem is elsewherePackage name vs. module name mismatch (pip install pillowimport PIL); check for a same-named local file shadowing the package
YesNoThe pip executable is bound to a different interpreterReinstall with python -m pip install → section 3, ③
NoYesBoth are in the wrong environment (activation failed / shims not refreshed)Section 3, ①·②
NoNoFully split stateClean up ① then ③ in section 3; if that fails, recreate the venv in section 5

That’s 30 seconds. Now we pin the branch.

Six mismatch branches: identification signal → one-line fix → recurrence prevention

Each item uses a fixed format: identification signal (healthy/unhealthy pair) → one-line recovery command → one-line recurrence prevention.

① Mixing system python and a venv

Identification signal

Bash
python -c "import sys; print(sys.executable); print(sys.prefix != sys.base_prefix)"
  • Healthy: /home/dev/proj/.venv/bin/python / True
  • Unhealthy: /usr/bin/python3 / False

If sys.prefix != sys.base_prefix is False, you are not inside a venv. That is true even if the prompt shows (.venv). Opening a new shell, starting a fresh tmux/screen, or running from a context that does not inherit the environment (sudo, make, a systemd unit) produces this.

One-line recovery

Bash
source /home/dev/proj/.venv/bin/activate && python -c "import sys; print(sys.executable)"

One-line recurrence prevention: In scripts, cron, and systemd, do not rely on activate — invoke the absolute-path interpreter directly, e.g. /home/dev/proj/.venv/bin/python script.py.

② pyenv shims not refreshed

Identification signal

Bash
pyenv which python
pyenv version
ls ~/.pyenv/shims | head
  • Healthy: pyenv which python/home/dev/.pyenv/versions/3.12.4/bin/python, pyenv version3.12.4 (set by /home/dev/proj/.python-version)
  • Unhealthy: pyenv: python: command not found, or a CLI you just installed is command not found even though pip show says it is installed

pyenv puts shims (relay scripts) on PATH instead of the real executables. When a new package installs a console script, you must rebuild shims for it to be recognized.

One-line recovery

Bash
pyenv rehash && pyenv which python

One-line recurrence prevention: Confirm eval "$(pyenv init -)" is in your shell rc, and on top of pyenv default to a per-project venv (python -m venv .venv) instead of global installs.

③ The pip executable is bound to a different interpreter

Identification signal

Bash
head -1 "$(which pip)"
  • Healthy: #!/home/dev/proj/.venv/bin/python
  • Unhealthy: #!/usr/bin/python3 (you’re in a venv, but the shebang is system Python)

The first line of the pip executable (the shebang) is “the interpreter this pip serves.” This often breaks after copying/moving a venv to another path, or after upgrading the base Python that created the venv.

One-line recovery

Bash
python -m pip install --force-reinstall pip

(If the shebang is still unchanged, recreate the venv — section 5 procedure)

One-line recurrence prevention: From now on, never type pip directly — always use python -m pip.

--user install is off PATH

Identification signal

Bash
python -m site --user-site
python -m pip show black | grep -i location
echo "$PATH" | tr ':' '\n' | grep -i local
  • Healthy: the installed package’s Location is the venv site-packages, and the console script runs immediately
  • Unhealthy: Location: /home/dev/.local/lib/python3.12/site-packages but black: command not found

Installing with --user (or the PIP_USER=1 environment variable) puts files in ~/.local and executables in ~/.local/bin. If that path is not on PATH, you get “installed but won’t run.” Inside a venv, --user is not supported at all, so the fact that --user was used is also a signal that you are outside a venv.

One-line recovery

Bash
export PATH="$HOME/.local/bin:$PATH"

One-line recurrence prevention: Check pip config list for a stuck user = true setting; install CLI tools with pipx install instead of --user, and isolate project dependencies in a venv.

⑤ Installed into the root area with sudo pip

Identification signal

Bash
ls -l /usr/local/lib/python3.12/site-packages | head
python -m pip show requests | grep -i location
  • Healthy: project packages exist only under the venv, owned by a regular user
  • Unhealthy: files under /usr/local/lib/python3.*/site-packages are owned by root; pip install --upgrade as a regular user hits Permission denied

sudo pip can overwrite files managed by the OS package manager (apt/dnf/brew) and break OS tools. Cleanup order:

  1. Record the current state with sudo python3 -m pip list --format=freeze > /tmp/root-pip.txt
  2. Reinstall only what the project needs into a venv (python -m pip install -r requirements.txt)
  3. Verify the application still works
  4. Only then remove from the root area (sudo python3 -m pip uninstall <pkg>) — do not touch packages that apt installed

One-line recovery

Bash
python -m venv .venv && source .venv/bin/activate && python -m pip install -r requirements.txt

One-line recurrence prevention: Block the strings sudo pip / sudo pip3 with a pre-commit hook or a shell alias (see section 5).

⑥ The IDE or Jupyter kernel selected a different interpreter

Identification signal — run this inside the editor/notebook, not the terminal.

Python
import sys; print(sys.executable)
  • Healthy: /home/dev/proj/.venv/bin/python (same as the terminal)
  • Unhealthy: /usr/bin/python3 or /opt/homebrew/bin/python3.11 (differs from the terminal)

VS Code’s Python extension auto-detect has improved, but opening the workspace from a parent folder or creating the venv later still leaves it stuck on an old interpreter.

One-line recovery: In VS Code, Ctrl/Cmd+Shift+PPython: Select Interpreter → pick .venv/bin/python, then restart both the terminal and the kernel (in PyCharm, set the same path under Settings → Project → Python Interpreter)

One-line recurrence prevention: Commit .vscode/settings.json at the project root to pin the interpreter.

JSON
{
  "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
  "python.terminal.activateEnvironment": true
}

Environment-specific differences and a Jupyter-only section

macOS (Homebrew + Command Line Tools coexisting)

Bash
which -a python3
/opt/homebrew/bin/python3 -V
/usr/bin/python3 -V
brew list --versions python@3.12
  • Healthy: /opt/homebrew/bin/python3 is first and the version is the intended one
  • Unhealthy: /usr/bin/python3 (Apple-provided CLT Python) is first → check PATH order in .zshrc

Apple’s /usr/bin/python3 is for OS tools; avoid the habit of installing packages into it. Cases where install is blocked because recent macOS and Homebrew tightened protection of system/managed environments are covered in the PEP 668 externally-managed-environment post.

Ubuntu 24.04

Bash
python3 -c "import sys; print(sys.path)"
dpkg -S /usr/lib/python3/dist-packages/yaml 2>/dev/null
apt list --installed 2>/dev/null | grep python3-

On Debian-family systems, apt-installed packages go to /usr/lib/python3/dist-packages and pip-installed ones go to site-packages or ~/.local. The paths are completely different. So if you apt install python3-requests and then import requests inside a venv, it is missing (a venv does not inherit system packages by default).

  • Healthy: all project dependencies live in one place — the venv site-packages
  • Unhealthy: some in dist-packages, some in the venv → the seed of version conflicts

Do not touch system Python; always create a venv.

Bash
sudo apt install -y python3-venv
python3 -m venv .venv && source .venv/bin/activate && python -m pip install -U pip

Windows (PowerShell)

Windows has a powerful tool called the launcher (py).

POWERSHELL
py -0p

Healthy output example

TEXT
 -V:3.12 *        C:\Users\dev\AppData\Local\Programs\Python\Python312\python.exe
 -V:3.11          C:\Users\dev\AppData\Local\Programs\Python\Python311\python.exe

The one with * is the default. When several versions are installed and you’re unsure where things went, pin the install target by version.

POWERSHELL
py -3.12 -m pip install requests
py -3.12 -c "import requests; print(requests.__file__)"

When using a venv:

POWERSHELL
py -3.12 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -c "import sys; print(sys.executable)"
  • Healthy: C:\proj\.venv\Scripts\python.exe
  • Unhealthy: C:\Users\dev\AppData\Local\Microsoft\WindowsApps\python.exe → the Microsoft Store stub was picked up. Turn off the Python aliases under Settings → Apps → App execution aliases.

Docker (COPY of a venv in a multi-stage build)

Scripts inside a venv have the absolute path from creation time baked into their shebang. If you create at /opt/venv in the builder stage and COPY to a different path in the runtime stage, the shebang breaks. Keep the path identical on both sides.

Dockerfile
FROM python:3.12-slim AS builder
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN python -m pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . .
CMD ["python", "-m", "app"]

Verify:

Bash
docker run --rm myimage python -c "import sys; print(sys.executable); print(sys.prefix != sys.base_prefix)"
  • Healthy: /opt/venv/bin/python / True
  • Unhealthy: /usr/local/bin/python / False → missing ENV PATH or COPY path mismatch

Jupyter: !pip and %pip are completely different

This is the most common trap in notebooks.

CommandWho runs itInstall target
!pip install XSubshell → the pip on the shell PATHMay be an interpreter unrelated to the kernel
%pip install XIPython magic → the current kernel interpreter’s pipAlways matches the kernel
!{sys.executable} -m pip install XKernel interpreter specified directlyAlways matches the kernel

So if you !pip install and then import fails, that is not a bug — it is working as designed. Use %pip in notebooks.

To see which interpreter the kernel uses:

Bash
jupyter kernelspec list
cat ~/.local/share/jupyter/kernels/proj-venv/kernel.json

Healthy output example

JSON
{
  "argv": ["/home/dev/proj/.venv/bin/python", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
  "display_name": "Python (proj-venv)",
  "language": "python"
}

Unhealthy output example

JSON
{
  "argv": ["/usr/bin/python3", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
  "display_name": "Python 3 (ipykernel)",
  "language": "python"
}

If argv[0] is not your venv, re-register the kernel.

Bash
source .venv/bin/activate
python -m pip install ipykernel
python -m ipykernel install --user --name proj-venv --display-name "Python (proj-venv)"

After registering, switch the notebook kernel to Python (proj-venv), restart, then verify with import sys; print(sys.executable). Done.

Why python -m pip is the right answer (one paragraph of principle)

pip is a separate executable sitting somewhere on PATH; you cannot know which interpreter it is bound to until you open the shebang. -m, by contrast, means “have this currently running interpreter find the pip module on its own sys.path and run it.” The runner is the install target, so coordinate A and coordinate B always match by definition. That one line structurally eliminates branches ②③④ of the six. The rapid spread of uv and pipx is an extension of the same philosophy: force the interpreter and the install target to stay bound together.

Team standardization checklist and last resort

Four things to lock in for the team

  • Pin the venv path: use only .venv at the project root. Add .venv/ to .gitignore, and put the creation command on the first line of the README
  • Mandate python -m pip: replace every pip install in CI scripts, Makefiles, and docs with python -m pip install
  • Pin versions: freeze requirements.txt with == and treat python -m pip freeze > requirements.lock.txt as an artifact
  • Block sudo pip: stop it at commit time with the hook below
YAML
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: no-sudo-pip
        name: block sudo pip in scripts
        entry: bash -c 'grep -rInE "sudo +pip3?" --include="*.sh" --include="Makefile" --include="*.md" . && exit 1 || exit 0'
        language: system
        pass_filenames: false

Last resort: recreate the venv (rename first, don’t delete)

If diagnosis doesn’t catch it, recreate. The important part is backing up before you wipe.

Bash
# 1) Back up current state
python -m pip freeze > requirements.lock.txt

# 2) Rename, don’t delete (so you can roll back)
mv .venv .venv.bak

# 3) Create a new venv
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip setuptools wheel

# 4) Restore
python -m pip install -r requirements.lock.txt

# 5) Verify
python -c "import sys; print(sys.executable); print(sys.prefix != sys.base_prefix)"
python -m pip -V
head -1 "$(which pip)"

# 6) If healthy, remove the old venv
rm -rf .venv.bak

At step 5, if sys.executable is the new .venv/bin/python, the second value is True, and the pip shebang is the same path, you’re done. If a specific package fails to install at step 4, that is a dependency-resolution problem, not a path problem — approach it on a separate axis. If you hit a certificate error at download time, see the SSLCertVerificationError post.

One-page summary card

TEXT
1. python -c "import sys; print(sys.executable)"
2. python -m pip -V   /   pip -V
3. If the two values differ → reinstall with python -m pip
4. If sys.executable is not the intended venv → check activation/kernel/shebang
5. When in doubt, always python -m pip

The next post (Python Development Guide part 9) covers the case where the problem is versions, not paths — dependency conflicts typified by ResolutionImpossible, and pinning strategy.

Frequently asked questions (FAQ)

Q. pip list clearly shows it, but only import fails. Why? A. The most likely explanation is that the pip that showed pip list and the python that tried import are different interpreters. Recheck with python -m pip list. If it disappears from that list, confirmed. If it shows up in both, suspect a package-name vs. module-name mismatch (pip install pillowimport PIL, pip install beautifulsoup4import bs4), or a same-named .py file in the working directory shadowing the package.

Q. I ran !pip install in a Jupyter notebook — why doesn’t import work? A. !pip starts a subshell and runs the pip on the shell PATH, which may be unrelated to the interpreter the notebook kernel uses. %pip install (IPython magic) installs into the current kernel interpreter. If the kernel itself is already bound to the wrong interpreter, activate the venv and re-register the kernel with python -m ipykernel install --user --name proj-venv, then check argv[0] in kernel.json.

Q. Creating a venv every time is annoying — why isn’t --user install recommended? A. ~/.local is a space shared by every project, so the version project A needs collides with the version project B needs. Also, if ~/.local/bin (where the executables land) is not on PATH, you get “installed but the command can’t be found.” For CLI tools shared across projects use pipx; for project dependencies, isolate them in a venv (or uv). That combination has the lowest maintenance cost.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·
관련 공식 문서Python 공식 문서

Comments

Be the first to comment.