Fix Python venv bad interpreter After Moving a Project
On this page
You move a project and its pip file still exists, yet running it reports bad interpreter, cannot execute: required file not found, or Python raises FileNotFoundError. Check the interpreter path recorded in the script before changing permissions or reinstalling with the system pip.
This beginner-to-intermediate Linux/Python tutorial reproduces the failure, identifies the stale path, and keeps the old environment while rebuilding at the final location. It uses the standard library and pip bundled with venv, downloads no third-party packages, and changes neither system Python nor scheduled jobs.
1. Method: distinguish the entry script from its interpreter
The official venv explanation describes absolute interpreter paths in installed scripts: environments are generally not portable and should be recreated even when only their parent directory moves.
- An existing script does not guarantee that the interpreter in its shebang (
#!...) exists. - Activation mainly adjusts PATH in the current shell; it cannot repair an old shebang.
python -m pipcan establish whether the module still loads, but is not an environment migration strategy.- Preserve source, input, and dependency records, then create a fresh environment at its destination. Do not patch just the first line of one pip script.
If the command cannot be found at all, start with shutil.which and PATH preflight checks. Here the entry exists but cannot launch.
2. Setup: build a small demonstration project
Use Linux, bash/zsh, and an existing Python 3.9+ interpreter that can create a venv with pip. Examples were tested on Linux with Python 3.11.4. Choose a short parent path without spaces for the reproduction, use a new directory on repeat runs, and keep all commands in the same terminal.
- Create the environment, input, and a task that locates data beside its script:
python3 --version
python3 -m venv --help
mkdir venv-move-demo
cd venv-move-demo
demo_root="$PWD"
mkdir original
python3 -m venv original/.venv
printf 'alpha\nbeta\ngamma\n' > original/input.txt
cat > original/report.py <<'PY_REPORT'
import json
from pathlib import Path
import sys
source = Path(__file__).with_name("input.txt")
print(json.dumps({
"venv": sys.prefix != sys.base_prefix,
"records": len(source.read_text(encoding="utf-8").splitlines()),
"prefix": sys.prefix,
}, sort_keys=True))
PY_REPORT
original/.venv/bin/pip --version
original/.venv/bin/python original/report.py
original/.venv/bin/python -m pip --disable-pip-version-check \
--no-cache-dir freeze > original/requirements.txt
- The task prints JSON with
venvset totrue,recordsset to 3, andprefixset to the absoluteoriginal/.venvpath. - This environment has no extra dependencies, so
requirements.txtis normally empty. The pip freeze documentation distinguishes an installed-package listing from a lockfile. For real projects, prefer the existing dependency files or locking workflow; save recovery snapshots separately instead of overwriting them.
Stop if environment creation fails. If venv or ensurepip is missing, choose an existing interpreter that supports them or ask the administrator to provide the matching components. Do not install dependencies globally for this experiment.
3. Reproduce: pip exists after the move but cannot launch
- Rename the whole demonstration project and inspect the beginning of its entry script:
mv original moved
ls -l moved/.venv/bin/pip
head -n 4 moved/.venv/bin/pip
python3 - <<'PY_BROKEN'
from pathlib import Path
import subprocess
entry = Path("moved/.venv/bin/pip").absolute()
assert entry.is_file()
try:
result = subprocess.run([str(entry), "--version"], check=False)
except FileNotFoundError:
print("BROKEN: pip script exists, but launch raised ENOENT")
else:
raise SystemExit(f"Unexpected result: exit={result.returncode}")
PY_BROKEN
- In this short-path experiment, pip’s first line should still contain
original/.venv/bin/python, which no longer exists. Expect the check to print:
BROKEN: pip script exists, but launch raised ENOENT
The checker catches the expected error and exits 0; pip did not launch successfully. Running the entry directly in a shell can produce different error wording and exit codes.
Some pip entry points use a shell wrapper for long paths or paths containing spaces. The stale path may then appear after the first line, which is why the command inspects four lines. If your symptoms differ, do not attribute every ENOENT to venv: inspect the actual interpreter, symlinks, and script line endings.
4. Diagnose: call the environment interpreter directly
In this experiment only the directory name changed; the base Python installation remains available:
moved/.venv/bin/python -m pip --version
moved/.venv/bin/python moved/report.py
The pip module should still report its version and the task should read three lines. The interpreter entry remains usable while the pip script embeds an old path. The pip invocation guide explains that python -m pip loads pip with the specified interpreter.
Treat this as a diagnostic comparison. Moving across machines, removing the base Python, or incompatible binary extensions may break the interpreter too. Success here does not establish that the relocated environment is repaired.
5. Repair: create a new environment at its final path
- Keep
moved/.venv, createmoved/.venv-rebuilt, install the demonstration requirements, and check it:
test ! -e moved/.venv-rebuilt && \
python3 -m venv moved/.venv-rebuilt
moved/.venv-rebuilt/bin/python -m pip --disable-pip-version-check \
--no-cache-dir install --no-index -r moved/requirements.txt
moved/.venv-rebuilt/bin/python -m pip check
moved/.venv-rebuilt/bin/pip --version
moved/.venv-rebuilt/bin/python moved/report.py
The first command exits nonzero if .venv-rebuilt already exists. Stop rather than reusing an unknown environment. After a successful first run, the new pip should report a location under .venv-rebuilt; pip check should print No broken requirements found., and the task should still report three records.
- The requirements file is empty, so
--no-indexallows an offline installation-path check. The pip install option reference says this flag ignores package indexes. Nonempty production requirements need suitable local wheels or installation according to the project’s trusted-index policy. This empty-file experiment does not validate business dependencies. - Treat
.venv-rebuiltas the new environment’s final name: do not rename it to.venvafter validation, which changes absolute paths again. If the final name must be.venv, first stop jobs and preserve the old environment, then rebuild directly at that final.venvpath rather than moving a completed replacement.
For a uv-managed project, retain the uv project and dependency workflow instead of replacing its lock process with a separate pip requirements list.
6. Validate: results, environment path, and working directory
- Assert task results instead of checking only whether pip launches:
moved/.venv-rebuilt/bin/python moved/report.py > result.json
python3 - <<'PY_CHECK'
import json
from pathlib import Path
result = json.loads(Path("result.json").read_text(encoding="utf-8"))
assert result["venv"] is True
assert result["records"] == 3
assert Path(result["prefix"]) == Path("moved/.venv-rebuilt").absolute()
print("OK: rebuilt environment; records=3")
PY_CHECK
Expect OK: rebuilt environment; records=3. This checks that a virtual environment is active, the input is complete, and the task actually uses the replacement environment.
- In the same terminal, launch the task from
/:
(
cd /
"$demo_root/moved/.venv-rebuilt/bin/python" "$demo_root/moved/report.py"
)
It should still report three records because the input location comes from __file__, not the working directory.
- Give the demonstration task an entry point that explicitly uses the new interpreter:
cat > moved/run-report.sh <<'SH_RUN'
#!/bin/sh
set -eu
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
exec "$project_dir/.venv-rebuilt/bin/python" "$project_dir/report.py"
SH_RUN
chmod +x moved/run-report.sh
"$demo_root/moved/run-report.sh"
For real automation, first run the same entry manually as the job user, then switch job configuration through the established deployment workflow. Check environment variables with Linux scheduled-job path and working-directory troubleshooting; this tutorial does not edit crontab. Retain the old environment for comparison, not as a working rollback. A real rollback restores the previous verified deployment and its original paths.
7. Troubleshooting
| Symptom | Concrete action |
|---|---|
pip exists but reports bad interpreter or ENOENT |
Inspect its initial interpreter path; rebuild at the final destination after a move. |
| Activation still points at the old directory | Stop sourcing the old activation script; validate with the replacement interpreter’s explicit path. |
Permission denied |
Inspect script permissions and parent-directory access first; do not hide path problems with chmod 777. |
No module named pip or unavailable ensurepip |
Stop and choose an existing interpreter that supports a pip-enabled venv; do not change system Python. |
ModuleNotFoundError in the replacement |
Install real requirements with the new environment’s python -m pip, then run project tests. The demo requirements are empty. |
Packages not found with --no-index |
Supply verified local wheels or use the project’s approved index, rather than copying old site-packages. |
| Shell command caching still selects the old entry | Run the new entry by absolute path first, then open a fresh terminal; do not rely on the prompt alone. |
Summary
After moving a project, diagnose bad interpreter by checking the entry script separately from its interpreter. Preserve dependency records, rebuild at the final path, and validate pip, task results, and execution from another working directory. Move code and dependency definitions rather than treating a whole venv as a portable deployment artifact.
- Author: yanzhi
- Original article: https://www.bobobk.com/en/python-venv-moved-bad-interpreter.html
- License: This work is licensed under CC BY-NC-ND 4.0. See the license for attribution, noncommercial use and no-derivatives terms.
Related articles
- Python argparse Subcommands: Required Operations, Bounded Options, and Dash-Prefixed Filenames
- Fix Python SQLite Foreign Keys: Connection Checks, Batch Rollback, and Orphan Audits
- Diagnose CSV Delimiters with Python: Sniffer, Explicit Formats, and Full Validation
- Check Executable Paths with Python shutil.which Before Automation Runs
- Validate INI Settings with Python configparser: Percent Signs, Booleans, and Duplicates