DEV Community

Taylor Wang
Taylor Wang

Posted on

pip Said Requirement Already Satisfied. It Was Satisfying the Wrong Interpreter.

Have you ever watched pip print a cheerful success line, then watched the next import crash anyway? I reconstructed that mismatch as a forty-eight hour lab diary, and it was not a network problem. It was two Pythons pretending to be one toolchain, plus a distro policy I had stopped reading. This is the notebook I wish I had opened at hour zero, before another install.

Hour 0: A Script That Should Have Been Boring

I needed a tiny checker that compared pinned versions in a requirements-style line. The first import was from packaging.version import Version, which should have been a one-liner. I ran the file with python check_pin.py and got ModuleNotFoundError: No module named 'packaging'. Nothing in that sentence deserved a two-day detour, right?

The script lived in a throwaway directory beside a venv I had created two weeks earlier. I opened a fresh terminal like a person with no memory, then trusted every short command name on PATH. Locally that feels harmless. It is how the wrong installer gets a vote.

What I Tried Before I Wrote Anything Down

I did the usual local dance, in this order, without capturing a single path:

  1. pip install packaging
  2. rerun python check_pin.py
  3. stare at the same traceback
  4. pip install --upgrade packaging
  5. stare harder, then blame PyPI

The install line kept saying the requirement was already satisfied. Why would an installer lie like that, and why would it lie so politely? I assumed caches, then assumed a yanked wheel, then assumed I had typed the package name wrong. I had typed pip and python as if they shared a prefix. They did not share a prefix.

Hour 4: I Asked a Model, Then I Left the Laptop

I pasted the traceback into MonkeyCode because this account has free model access, and I wanted a clean second opinion. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I reran the same commands on the free server option so the failure would have a remote column, not just my shell history. The model did what most coding models do under time pressure. It suggested pip install packaging, then pip install --user packaging, then a root install I will not repeat here.

That advice is locally tempting. It is also how you spend the next day fighting the operating system instead of the import. The traceback I had pasted never included sys.executable. That omission is on me.

Hour 6: The Server Spoke PEP 668

On the disposable runner, the first pip install did not even pretend to succeed. It printed the externally-managed-environment error that Debian and Ubuntu still ship after PEP 668. The stdlib directory contained an EXTERNALLY-MANAGED marker, and pip 23+ refuses to write over a distro Python without a break-glass flag. I had been treating that message as noise. It is the distro telling you that system python3 is not a playground.

I created a virtual environment, because that is the actual fix, not a louder installer:

python3 -m venv .venv
. .venv/bin/activate
python -m pip install packaging
python check_pin.py
Enter fullscreen mode Exit fullscreen mode

The import worked inside that shell. I closed the session feeling clever, which is usually the hour the bug relocates to another process.

Hour 18: Requirement Already Satisfied

Back on the laptop, pip still claimed packaging was satisfied, and python check_pin.py still crashed. I finally printed the only identity checks that matter, in one block:

which python
which pip
python -c "import sys; print(sys.executable)"
pip -V
python -m pip -V
Enter fullscreen mode Exit fullscreen mode

pip -V pointed at a user-level installer hanging off a different prefix. python -m pip -V pointed at the interpreter that actually ran my script. The package was installed. It was installed for someone else. Have you checked those two -V lines on the same morning, or do you also trust the short name?

I uninstalled from the wrong place, then installed with the interpreter that would run the file:

python -m pip uninstall -y packaging
python -m pip install packaging
python -c "from packaging.version import Version; print(Version('1.2.3'))"
Enter fullscreen mode Exit fullscreen mode

The version object printed, and I still did not trust the shell. I wrote a probe so the next session would start with paths, not folklore.

The Artifact I Wish I Had Run First

Save this as interp_probe.py. It needs no network, and it is meant to be boring. Run it with every launcher you currently believe is "python".

#!/usr/bin/env python3
"""Show which interpreter will own the next pip install."""

from __future__ import annotations

import sys
import sysconfig
from pathlib import Path


def main() -> None:
    stdlib = Path(sysconfig.get_path("stdlib"))
    marker = stdlib / "EXTERNALLY-MANAGED"
    purelib = Path(sysconfig.get_path("purelib"))
    in_venv = sys.prefix != sys.base_prefix

    print(f"executable: {sys.executable}")
    print(f"version: {sys.version.split()[0]}")
    print(f"prefix: {sys.prefix}")
    print(f"base_prefix: {sys.base_prefix}")
    print(f"in_venv: {in_venv}")
    print(f"purelib: {purelib}")
    print(f"pep668_marker: {marker}")
    print(f"pep668_active: {marker.is_file()}")
    try:
        import packaging

        print(f"packaging_file: {packaging.__file__}")
    except Exception as exc:  # lab probe: show any import failure
        print(f"packaging_import: {type(exc).__name__}: {exc}")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Three launchers, one file

python interp_probe.py
python3 interp_probe.py
python -m interp_probe
Enter fullscreen mode Exit fullscreen mode

If those three blocks disagree, stop installing things. You are not debugging the packaging project. You are debugging launchers that happen to share a nickname.

The checklist I now keep beside the probe

  • Always call python -m pip, never a bare pip, when the next import matters.
  • Treat Requirement already satisfied as a question, not as a green check.
  • If PEP 668 fires, create a venv; do not reach for --break-system-packages.
  • After activate, run the probe again, because activation is per shell.
  • On a remote runner, activation often dies between queued commands.

A Decision Table I Now Keep in the Repo

What you see What it usually means What to run next
ModuleNotFoundError after a green pip pip is not that interpreter python -m pip -V versus pip -V
externally-managed-environment distro Python, PEP 668 marker present python3 -m venv .venv, then install inside it
Requirement already satisfied plus ImportError another prefix owns the dist-info python -m pip show packaging
Works in one SSH command, fails in the next venv activate did not survive print VIRTUAL_ENV and sys.executable every session
Model suggests sudo pip or --user on a server it is optimizing for a laptop story refuse, recreate the venv, rerun the probe

None of those rows need a benchmark. They need two paths printed next to each other, on the same machine, in the same minute.

What Broke When I Trusted the Shell

The remote column made the PEP 668 failure obvious, which was useful. The laptop hid it behind a user-site pip that still had an old wheel. The model collapsed both machines into one advice string, because the traceback did not include interpreter identity. I had pasted symptoms and withheld the only fact that could choose an installer.

Non-interactive runners added a second cut that local terminals rarely teach you. I activated .venv in command one, then ran python check_pin.py in command two, and the second process was a clean shell. Does your runner inherit activation, or does it start empty every time? Mine started empty. The probe in command two showed in_venv: False, and I finally stopped blaming the index.

What I Would Repeat

I would print interpreter identity before I paste any traceback into a model. I would install with python -m pip only, even when the short name looks faster. I would create a venv on every machine that is not already a dedicated environment manager. I would run the probe on the laptop and on the disposable runner, then diff executable and pep668_active by hand.

If you like current tooling, uv can create the venv and install without teaching the shell two names:

uv venv .venv
uv pip install packaging
.venv/bin/python interp_probe.py
Enter fullscreen mode Exit fullscreen mode

I still run the probe after uv, because confidence is how this bug returns. A lockfile would have helped the dependency set. It would not have helped a bare pip pointed at the wrong prefix.

Limitations, and Who Should Skip This

This notebook does not fix missing manylinux tags, broken C extensions, or a wheel that never uploaded for your platform. It also does not replace a lockfile, and it will not explain an import that fails after the probe says the file is present. If your shop standardizes on Conda and never wants a venv, the probe still helps, but the install commands on this page will not match your runbooks.

Do not paste tokens into a shared or free runner to "just test pip". The lesson is interpreter identity, not secret handling. Skip the model loop entirely when you already know pip -V and python -m pip -V disagree. The table above is faster than another generated install line. Skip --break-system-packages unless you are maintaining the distro image itself, which I was not.

I am not claiming a quota, a model name, or a hardware spec for the remote side. I am claiming that a second machine will disagree with your laptop, and that disagreement is the useful part.

Hour 48: The Note I Left for Next Time

The package was never missing. The installer I trusted was never the interpreter I ran. PEP 668 was not a pip regression; it was a boundary I had walked past while chasing a satisfied requirement. Next time I will run interp_probe.py before I ask anything, including myself, why a green install still cannot import.

If you steal one habit from this diary, steal the double -V check and the three-way probe run. Diff those blocks before you argue with a satisfied requirement.

Top comments (0)