November 7, 2026•5 min read

Python venv: the Install That Went Into the Wrong Python

pip install printed Successfully installed, and the next run raised ModuleNotFoundError on the same machine. Two interpreters, one install, and the variable your script checks is the one the interpreter never sets.

Read in Indonesian

The install printed "Successfully installed". The script imported fine in one terminal. Then the editor's Run button, or a new shell, raised ModuleNotFoundError for the package you just installed, on the same machine and in the same project.

You have two interpreters on one machine, the system one and your project's venv, and pip install only touches the one that ran it.

Here is the measured version, on Linux with Python 3.14.9 on 2026-11-07. The venv module ships with Python, so there is nothing to install first (venv docs).

python3 -m venv .venv writes the venv into .venv/. Activating it is a shell change: source .venv/bin/activate on macOS and Linux, .venv\Scripts\activate on Windows. After that, python and pip mean the venv. Activation is convenient, not required, because .venv/bin/python is the same interpreter without it.

BASH
$ python3 --version
Python 3.14.9
$ python3 -m venv .venv
$ .venv/bin/pip install requests

The last line of the install output:

TEXT
Successfully installed certifi-2026.10.23 charset_normalizer-3.5.2 idna-3.20 requests-2.34.2 urllib3-2.8.0

One pip install requests pulled five packages, and du -sh .venv reported 9.3M. Now run the same one-line probe with each interpreter, on the same machine in the same second:

CONSOLE
$ .venv/bin/python -c "import requests; print(requests.__version__)"
2.34.2
$ python3 -c "import requests; print(requests.__version__)"
ModuleNotFoundError: No module named 'requests'

The first exits 0. The second exits 1. Nothing is wrong with the install. You asked a different Python.

The bug you will actually hit comes next. To find out which Python is running, many scripts read the shell variable VIRTUAL_ENV, which only exists while the venv is activated. Here is what that does.

Key takeaways

  • pip install only ever touches the interpreter that ran it, so a project with a venv has two Pythons and the package landed in one of them.
  • VIRTUAL_ENV is set by activation, not by the interpreter, so .venv/bin/python where.py still reports system.
  • sys.prefix != sys.base_prefix is the question to ask the interpreter itself, because it is true exactly when a venv started it.
  • The checker tests which interpreter is running, not whether your dependency versions are the right ones, and it cannot see the interpreter your editor runs.

Two files, one command

Save this as where.py. It decides by reading the environment, and it exits 0 only when it believes it is in a venv.

PYTHON
# where.py
import os
import sys


def which_interpreter():
    if os.environ.get("VIRTUAL_ENV"):
        return "venv"
    return "system"


def main():
    kind = which_interpreter()
    print(kind)
    return 0 if kind == "venv" else 1


raise SystemExit(main())

Save this next to it as check_where.py. It builds a throwaway venv, runs where.py with the venv's python and with the system one, and counts passes.

PYTHON
# check_where.py
import os
import subprocess
import sys
import tempfile

passed = 0
total = 0


def check(name, ok, detail=""):
    global passed, total
    total += 1
    if ok:
        passed += 1
        print(f"PASS  {name}")
    else:
        print(f"FAIL  {name}  <- {detail}")


root = os.path.dirname(os.path.abspath(__file__))
script = os.path.join(root, "where.py")

home = tempfile.mkdtemp(prefix="venvcheck-")
venv_dir = os.path.join(home, ".venv")
subprocess.run([sys.executable, "-m", "venv", venv_dir], check=True)
bindir = "Scripts" if os.name == "nt" else "bin"
venv_python = os.path.join(venv_dir, bindir, "python" + (".exe" if os.name == "nt" else ""))

inside = subprocess.run([venv_python, script], capture_output=True, text=True)
outside = subprocess.run([sys.executable, script], capture_output=True, text=True)

check("where.py runs when a venv calls it", inside.returncode == 0, f"exit {inside.returncode}")
check("a venv interpreter reports venv", inside.stdout.strip() == "venv", repr(inside.stdout.strip()))
check(
    "the system interpreter reports system",
    outside.returncode == 1 and outside.stdout.strip() == "system",
    f"exit {outside.returncode}, said {outside.stdout.strip()!r}",
)

print()
print(f"{passed} of {total} checks passed")
raise SystemExit(0 if passed == total else 1)

Run python3 check_where.py. It exits 1:

TEXT
FAIL  where.py runs when a venv calls it  <- exit 1
FAIL  a venv interpreter reports venv  <- 'system'
PASS  the system interpreter reports system

1 of 3 checks passed

The venv's own python ran the script, and it still said system. source .venv/bin/activate changes your shell so that python means the venv. Calling .venv/bin/python directly never touches that shell variable, so VIRTUAL_ENV is empty. The script asked the shell, and the shell was never told.

Ask the interpreter instead. Inside a venv, sys.prefix points at the venv and sys.base_prefix points at the Python it was built from. Replace where.py with:

PYTHON
# where.py
import sys


def inside_venv():
    return sys.prefix != sys.base_prefix


def main():
    kind = "venv" if inside_venv() else "system"
    print(kind)
    return 0 if kind == "venv" else 1


raise SystemExit(main())

Run python3 check_where.py again. It exits 0:

TEXT
PASS  where.py runs when a venv calls it
PASS  a venv interpreter reports venv
PASS  the system interpreter reports system

3 of 3 checks passed

What this does not do

The checker tests which interpreter is running. It does not test whether your dependencies are the right ones, and it does not lock versions. It also cannot see the interpreter your editor is configured with, so the Run button still needs its own check.

I only ran this on Linux. The Windows branch in the checker (Scripts, python.exe) is there for you to try, and I did not verify it.

One more trap: pip freeze in a brand-new venv prints nothing. An empty requirements file is not proof of a broken install. After the requests install above, pip freeze printed exactly this:

TEXT
certifi==2026.10.23
charset-normalizer==3.5.2
idna==3.20
requests==2.34.2
urllib3==2.8.0

Run the checker from inside the venv

Now run the same checker with the venv's own interpreter: .venv/bin/python check_where.py. One check fails, and the exit code is 1:

TEXT
PASS  where.py runs when a venv calls it
PASS  a venv interpreter reports venv
FAIL  the system interpreter reports system  <- exit 0, said 'venv'

2 of 3 checks passed

The checker compares against sys.executable. In this run that is the venv python, so "the system interpreter" was never tested.

Before you debug the next import error, print sys.executable from the script that failed. Which Python is it, and is that the one you installed into?

Get better at shipping code.

Join the other developers getting tech updates, tips, and relaxed tutorials in their inbox every week.

Stay in the loop

Tech updates, coding tips, and relaxed insights straight to your inbox every week.

No spam. Unsubscribe anytime.

Read next

See all
Struktur DataWhat a Linked List Actually Is

A linked list is a linear chain of nodes, each holding data and a pointer to the next node. Here is what that chain looks like, what it costs next to an array, and the same minimal implementation in Java, Python, and JavaScript.

For developers who learn by struggling

© 2026 Inva.dev.