It installed successfully — so why is it missing? The problem is coordinates, not the error
The terminal clearly printed this:
Successfully installed requests-2.32.3But 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.
which -a python python3 pip pip3python -c "import sys; print(sys.executable); print('---'); print('\n'.join(sys.path))"python -m pip -V
pip -Vpython -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)
/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/pip3The 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
/home/dev/proj/.venv/bin/python
/usr/bin/python3
/usr/local/bin/pip
/home/dev/proj/.venv/bin/pippython 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
/home/dev/proj/.venv/bin/python
---
/usr/lib/python3.12
/usr/lib/python3.12/lib-dynload
/home/dev/proj/.venv/lib/python3.12/site-packagesIf the last line contains the venv’s site-packages, you’re healthy.
Unhealthy output example
/usr/bin/python3
---
/usr/lib/python3.12
/usr/lib/python3.12/lib-dynload
/usr/lib/python3/dist-packagesYou 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
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
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)
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:
python -m pip show requests | grep -i locationHealthy: 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? | Verdict | Next action |
|---|---|---|---|
| Yes | Yes | Environment is consistent. The problem is elsewhere | Package name vs. module name mismatch (pip install pillow → import PIL); check for a same-named local file shadowing the package |
| Yes | No | The pip executable is bound to a different interpreter | Reinstall with python -m pip install → section 3, ③ |
| No | Yes | Both are in the wrong environment (activation failed / shims not refreshed) | Section 3, ①·② |
| No | No | Fully split state | Clean 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
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
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
pyenv which python
pyenv version
ls ~/.pyenv/shims | head- Healthy:
pyenv which python→/home/dev/.pyenv/versions/3.12.4/bin/python,pyenv version→3.12.4 (set by /home/dev/proj/.python-version) - Unhealthy:
pyenv: python: command not found, or a CLI you just installed iscommand not foundeven thoughpip showsays 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
pyenv rehash && pyenv which pythonOne-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
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
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
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-packagesbutblack: 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
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
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-packagesare owned byroot;pip install --upgradeas 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:
- Record the current state with
sudo python3 -m pip list --format=freeze > /tmp/root-pip.txt - Reinstall only what the project needs into a venv (
python -m pip install -r requirements.txt) - Verify the application still works
- Only then remove from the root area (
sudo python3 -m pip uninstall <pkg>) — do not touch packages that apt installed
One-line recovery
python -m venv .venv && source .venv/bin/activate && python -m pip install -r requirements.txtOne-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.
import sys; print(sys.executable)- Healthy:
/home/dev/proj/.venv/bin/python(same as the terminal) - Unhealthy:
/usr/bin/python3or/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+P → Python: 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.
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.terminal.activateEnvironment": true
}Environment-specific differences and a Jupyter-only section
macOS (Homebrew + Command Line Tools coexisting)
which -a python3
/opt/homebrew/bin/python3 -V
/usr/bin/python3 -V
brew list --versions python@3.12- Healthy:
/opt/homebrew/bin/python3is 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
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.
sudo apt install -y python3-venv
python3 -m venv .venv && source .venv/bin/activate && python -m pip install -U pipWindows (PowerShell)
Windows has a powerful tool called the launcher (py).
py -0pHealthy output example
-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.exeThe one with * is the default. When several versions are installed and you’re unsure where things went, pin the install target by version.
py -3.12 -m pip install requests
py -3.12 -c "import requests; print(requests.__file__)"When using a venv:
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 underSettings → 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.
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:
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→ missingENV PATHor COPY path mismatch
Jupyter: !pip and %pip are completely different
This is the most common trap in notebooks.
| Command | Who runs it | Install target |
|---|---|---|
!pip install X | Subshell → the pip on the shell PATH | May be an interpreter unrelated to the kernel |
%pip install X | IPython magic → the current kernel interpreter’s pip | Always matches the kernel |
!{sys.executable} -m pip install X | Kernel interpreter specified directly | Always 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:
jupyter kernelspec list
cat ~/.local/share/jupyter/kernels/proj-venv/kernel.jsonHealthy output example
{
"argv": ["/home/dev/proj/.venv/bin/python", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
"display_name": "Python (proj-venv)",
"language": "python"
}Unhealthy output example
{
"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.
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
.venvat the project root. Add.venv/to.gitignore, and put the creation command on the first line of the README - Mandate
python -m pip: replace everypip installin CI scripts, Makefiles, and docs withpython -m pip install - Pin versions: freeze
requirements.txtwith==and treatpython -m pip freeze > requirements.lock.txtas an artifact - Block
sudo pip: stop it at commit time with the hook below
# .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: falseLast 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.
# 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.bakAt 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
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 pipThe 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 pillow → import PIL, pip install beautifulsoup4 → import 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.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.