Python 基础体系 · 第 63/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。

Python 虚拟环境:venv、解释器隔离、路径和常见污染

Python 虚拟环境解决的不是“如何安装一个包”,而是一个更基础的问题:

当多个项目依赖同一个包的不同版本时,如何让每个项目使用自己的一组解释器、包和命令,而不相互覆盖?

例如:

  • 项目 A 需要 requests==2.31.*
  • 项目 B 需要 requests>=2.32
  • 系统环境中还可能存在其他项目安装的 requests
  • 当前 shell 里可能设置了 PYTHONPATH
  • 编辑器、测试工具和终端可能实际调用不同的 Python。

如果这些对象最终汇聚到同一个 site-packages 或同一条 sys.path 中,安装顺序就会影响运行结果。虚拟环境通过改变解释器的安装前缀和包搜索路径,把项目的运行时依赖拆分到不同目录中。


1. 先区分四个经常混用的概念

1.1 Python 解释器

解释器是实际执行 Python 字节码、导入模块和启动程序的可执行文件,例如:

/usr/bin/python3
/usr/local/bin/python3.14
/home/alice/.venvs/demo/bin/python
C:\Users\Alice\AppData\Local\Programs\Python\Python314\python.exe

运行下面的命令时,真正决定程序使用哪个 Python 的,是被操作系统找到并执行的那个文件:

python app.py

命令名 python 只是一个查找入口。它可能来自:

  • 当前虚拟环境的 bin/pythonScripts\python.exe
  • 系统安装目录;
  • 用户级 Python 管理器;
  • Conda、pyenv 或其他版本管理器;
  • shell alias 或函数。

因此,看到终端提示符中出现了 (.venv),并不能单独证明后续命令一定调用了预期解释器。可靠判断应直接检查解释器路径:

python -c "import sys; print(sys.executable); print(sys.version)"

预期结果类似:

/home/alice/project/.venv/bin/python
3.14.0 (...)

在 Windows PowerShell 中:

python -c "import sys; print(sys.executable); print(sys.version)"

如果输出的是系统 Python,而不是项目目录下的 .venv,那么当前 shell 并未使用目标虚拟环境。


1.2 Python 包

通常指可以被 import 的代码集合,例如:

import requests

但“导入名”和“安装名”不一定相同:

安装时的 distribution 名称 导入时的模块名
Pillow PIL
beautifulsoup4 bs4
python-dateutil dateutil

Python 包管理器安装的是 distribution distribution,发行包;Python 导入系统查找的是 sys.path 中的模块和包目录。

这一区分对于诊断污染很重要:

python -m pip show Pillow
python -c "import PIL; print(PIL.__file__)"

前一条检查发行包元数据,后一条检查实际被导入的文件。二者路径不一致时,通常存在解释器错用、路径覆盖或残留文件。


1.3 虚拟环境

Python Packaging Authority 将虚拟环境定义为:供特定应用使用、相对于系统范围安装更隔离的 Python 环境。标准虚拟环境模型由 PEP 405 引入;它通常包含一个 Python 可执行文件、一个独立的 site-packages 和一个 pyvenv.cfg 文件。虚拟环境通常共享基础 Python 的标准库,但拥有自己的第三方包目录。(packaging.python.org)

因此,虚拟环境不是:

  • 一台虚拟机;
  • 一个容器;
  • 一个新的操作系统;
  • 一份完整复制的 Python 安装;
  • 自动锁定依赖版本的机制。

它主要隔离的是:

  1. 项目使用的 Python 入口;
  2. 第三方包安装位置;
  3. 由包安装生成的命令行脚本;
  4. 解释器用于计算安装路径时的前缀。

1.4 激活

激活是修改当前 shell 状态的一组脚本操作,典型效果包括:

  • 将虚拟环境的可执行文件目录放到 PATH 前面;
  • 设置 VIRTUAL_ENV
  • 修改命令提示符;
  • 使 pythonpip 更可能解析到虚拟环境。

激活不是虚拟环境本身,也不是 Python 解释器的运行模式。Python 程序即使没有激活,也可以直接使用虚拟环境中的解释器:

.venv/bin/python app.py
.venv/bin/python -m pip install requests

Windows PowerShell:

.\.venv\Scripts\python.exe app.py
.\.venv\Scripts\python.exe -m pip install requests

Python 文档明确指出,激活步骤是可选的;判断程序是否处于虚拟环境中,应使用解释器运行时的前缀,而不能依赖 shell 激活带来的变化。(packaging.python.org)


2. venv 创建了什么

在 Python 3.14 中,最基本的创建命令是:

python3.14 -m venv .venv

这里有一个容易被忽略的因果关系:

venv 使用的是执行 python3.14 -m venv 的那个解释器版本。

如果执行命令的是 Python 3.14,环境中的 Python 就以这个 3.14 解释器为基础;如果执行命令的是 Python 3.13,环境就不会因为目录名叫 .venv 而自动变成 3.14。Python 教程也明确说明,venv 使用执行命令时的 Python 版本。(docs.python.org)

创建后,Unix 系统大致会得到:

project/
├── .venv/
│   ├── bin/
│   │   ├── python
│   │   ├── pip
│   │   └── activate
│   ├── include/
│   ├── lib/
│   │   └── python3.14/
│   │       └── site-packages/
│   ├── lib64 -> lib
│   └── pyvenv.cfg
└── app.py

Windows 的目录名称通常是:

project\
├── .venv\
│   ├── Include\
│   ├── Lib\
│   │   └── site-packages\
│   ├── Scripts\
│   │   ├── python.exe
│   │   ├── pip.exe
│   │   └── Activate.ps1
│   └── pyvenv.cfg
└── app.py

pyvenv.cfg 是关键元数据文件,内容通常类似:

home = /usr/local/bin
include-system-site-packages = false
version = 3.14.0

其中:

  • home 指向创建该环境时所使用的基础 Python 安装位置;
  • include-system-site-packages = false 表示默认不搜索系统级 site-packages
  • version 描述创建时的 Python 版本信息。

标准文档将虚拟环境视为可丢弃、不可移动的目录:不应把项目代码放进去,也不应把它当作可复制部署物。目标位置变化后,应该重新创建环境。(docs.python.org)


3. venv 并没有复制整个 Python

虚拟环境的“隔离”需要精确定义。

假设:

基础 Python:
/opt/python/3.14/

项目虚拟环境:
/home/alice/project/.venv/

创建 .venv 后,虚拟环境通常拥有:

/home/alice/project/.venv/bin/python
/home/alice/project/.venv/lib/python3.14/site-packages/

但标准库通常仍来自基础 Python:

/opt/python/3.14/lib/python3.14/

可以把它抽象为:

E=(IE,PE,SB,B)E = (I_E, P_E, S_B, B)

其中:

  • EE:虚拟环境;
  • IEI_E:环境自己的解释器入口;
  • PEP_E:环境自己的第三方包目录;
  • SBS_B:基础 Python 提供的标准库;
  • BB:基础 Python 安装位置。

默认情况下:

可导入模块=SBPE\text{可导入模块} = S_B \cup P_E

而不是:

SBPEPsystemPuserS_B \cup P_E \cup P_{\text{system}} \cup P_{\text{user}}

这就是“共享标准库、隔离第三方包”的基本模型。PyPA 虚拟环境规范也将“独立 Python 二进制文件、独立包目录、共享标准库”作为标准模型的核心组成。(packaging.python.org)

这意味着:

  • 删除虚拟环境通常不会删除基础 Python 的标准库;
  • 基础 Python 被删除或升级到不兼容状态,旧虚拟环境可能失效;
  • 安装包到 .venv 通常不会修改系统 site-packages
  • .venv 不是完全自包含的二进制分发物。

4. sys.prefix 如何标识虚拟环境

Python 运行时有两组重要的前缀:

import sys

print("sys.executable:", sys.executable)
print("sys.prefix:", sys.prefix)
print("sys.base_prefix:", sys.base_prefix)
print("sys.exec_prefix:", sys.exec_prefix)
print("sys.base_exec_prefix:", sys.base_exec_prefix)

在基础环境中,通常满足:

sys.prefix=sys.base_prefix\texttt{sys.prefix} = \texttt{sys.base\_prefix}

在虚拟环境中,通常满足:

sys.prefixsys.base_prefix\texttt{sys.prefix} \ne \texttt{sys.base\_prefix}

其中:

  • sys.prefix:当前运行环境的前缀;
  • sys.base_prefix:基础 Python 的前缀;
  • sys.exec_prefixsys.base_exec_prefix:对应平台相关库和扩展模块的前缀。

Python 文档将 sys.prefix != sys.base_prefix 作为运行时识别虚拟环境的可靠方式之一。虚拟环境规范也采用这一判定模型。(packaging.python.org)

可以用下面的检查函数:

import sys

def describe_runtime() -> None:
    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"running in venv = {in_venv}")

describe_runtime()

典型输出:

executable      = /home/alice/project/.venv/bin/python
version         = 3.14.0
prefix          = /home/alice/project/.venv
base_prefix     = /opt/python/3.14
running in venv = True

注意:VIRTUAL_ENV 环境变量只能说明某个激活脚本曾经设置过该变量。它可能与真正执行的 sys.executable 不一致,因此不能作为 Python 程序判断环境的唯一依据。


5. Python 启动时如何构造 sys.path

sys.path 是 Python 导入模块时依次搜索的路径列表:

import sys

for index, path in enumerate(sys.path):
    print(index, repr(path))

一个简化后的构造过程如下:

flowchart TD
    A[启动 python] --> B[确定脚本目录或当前目录]
    B --> C[读取 PYTHONPATH]
    C --> D[确定基础 Python 的标准库路径]
    D --> E[检查 pyvenv.cfg]
    E --> F[设置 sys.prefix 与 sys.base_prefix]
    F --> G[处理 site 模块]
    G --> H[加入虚拟环境 site-packages]
    H --> I{include-system-site-packages=true?}
    I -- 是 --> J[加入系统 site-packages]
    I -- 否 --> K[不加入系统 site-packages]
    J --> L[处理 .pth、sitecustomize、usercustomize]
    K --> L
    L --> M[得到最终 sys.path]

更严格地说,Python 3.14 的路径初始化大致包含以下来源:

5.1 脚本目录或当前目录

执行:

python app.py

sys.path 的首项通常是 app.py 所在目录。

执行:

python -m package.module

或者:

python
python -c "..."

首项通常与当前工作目录有关。

这解释了一个常见污染:

project/
├── json.py
└── app.py

执行:

python app.py

如果 app.py 中有:

import json

Python 可能优先导入项目目录中的 json.py,而不是标准库的 json。因此“虚拟环境隔离”并不等于“当前目录隔离”。

5.2 PYTHONPATH

如果设置了:

export PYTHONPATH=/home/alice/shared

Python 会把该目录加入模块搜索路径。Python 官方文档特别提醒,PYTHONPATH 会影响所有已安装的 Python 版本和虚拟环境,不应轻易在 shell 配置文件或全局环境变量中设置。(docs.python.org)

检查方法:

python -c "import os, sys; print('PYTHONPATH =', os.environ.get('PYTHONPATH')); print(*sys.path, sep='\n')"

清除当前 shell 中的设置:

unset PYTHONPATH

PowerShell:

Remove-Item Env:PYTHONPATH

5.3 标准库和扩展模块

Python 会根据基础安装位置寻找:

  • 平台无关的标准库;
  • 平台相关的扩展模块;
  • 其他解释器运行所需的目录。

虚拟环境通过 pyvenv.cfg 使 sys.prefix 指向环境目录,同时保留 sys.base_prefix 指向基础 Python。Python 3.14 已经把虚拟环境前缀的设置前移到路径初始化阶段,而不再依赖 site 模块。(docs.python.org)

5.4 site-packages

最后,site 模块会加入第三方包目录,并处理其中的路径配置文件。

虚拟环境默认的第三方包目录类似:

Linux/macOS:
.venv/lib/python3.14/site-packages/

Windows:
.venv\Lib\site-packages\

精确路径不要靠目录猜测,应使用解释器查询:

python -c "import sysconfig; print(sysconfig.get_path('purelib')); print(sysconfig.get_path('platlib'))"
  • purelib:纯 Python 包的安装目录;
  • platlib:平台相关包或扩展模块的安装目录。

使用 sysconfig 查询的原因是不同操作系统、发行版、架构和 free-threaded 构建可能采用不同目录布局。


6. pip 为什么应该写成 python -m pip

最常见的包安装错误是:

pip install requests
python app.py

这里的 pippython 可能不是同一个环境中的程序。

例如:

pip      -> /usr/local/bin/pip
python   -> /home/alice/project/.venv/bin/python

结果是:包被安装到系统环境,而项目解释器仍然找不到它。

更可靠的写法是:

python -m pip install requests

它表达的是:

使用当前这个 python 解释器加载并执行它对应的 pip 模块。

完整验证:

python -c "import sys; print(sys.executable)"
python -m pip --version
python -m pip show requests
python -c "import requests; print(requests.__file__)"

预期情况下,几个路径都应指向同一个 .venv

/home/alice/project/.venv/bin/python
pip 25.x from /home/alice/project/.venv/lib/python3.14/site-packages/pip (...)
Location: /home/alice/project/.venv/lib/python3.14/site-packages
/home/alice/project/.venv/lib/python3.14/site-packages/requests/__init__.py

这里需要区分两个事实:

  • python -m pip 能显著降低“pip 和 python 不匹配”的风险;
  • 它不能修复 PYTHONPATH、当前目录同名模块、--system-site-packages.pth 文件造成的路径污染。

7. 激活、直接调用和命令脚本

7.1 Unix shell

创建并激活:

python3.14 -m venv .venv
source .venv/bin/activate

检查:

command -v python
command -v pip
echo "$VIRTUAL_ENV"

退出:

deactivate

激活脚本的本质是修改当前 shell 的状态,而不是修改 .venv 中的 Python 二进制文件。

7.2 Windows PowerShell

创建并激活:

py -3.14 -m venv .venv
.\.venv\Scripts\Activate.ps1

如果执行策略阻止脚本运行,Python 文档给出的用户级设置方式是:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

也可以完全不激活,直接调用:

.\.venv\Scripts\python.exe -m pip install requests
.\.venv\Scripts\python.exe app.py

7.3 为什么安装的命令能直接运行

当包提供命令行入口时,安装工具通常会在环境的可执行文件目录生成脚本:

.venv/bin/tool
.venv/Scripts/tool.exe

这些脚本通常包含指向虚拟环境解释器的 shebang 或启动信息。Python venv 文档也说明,虚拟环境中安装的脚本会指向环境解释器。(docs.python.org)

因此,激活后运行:

tool

依赖的是 PATH 顺序;而直接运行:

.venv/bin/tool

依赖的是该脚本自身记录的解释器路径。

如果把 .venv/home/alice/project 移动到 /tmp/project,脚本中的解释器路径可能仍然指向旧位置。这是虚拟环境“不应移动或复制”的主要原因之一。


8. --system-site-packages 如何打破默认隔离

默认创建:

python3.14 -m venv .venv

相当于:

include-system-site-packages = false

创建时显式指定:

python3.14 -m venv --system-site-packages .venv

则会允许环境访问系统级 site-packages

include-system-site-packages = true

此时可导入集合近似变为:

可导入模块=SBPEPsystem\text{可导入模块} = S_B \cup P_E \cup P_{\text{system}}

这会产生一个危险现象:

python -c "import requests; print(requests.__file__)"

即使你从未在 .venv 中安装 requests,导入也可能成功,因为它来自系统环境。

这类环境的失败往往不是“包不存在”,而是:

  • 本地环境测试通过;
  • 换一台机器后导入失败;
  • CI 中找不到包;
  • 同一个包在系统环境升级后,项目行为发生变化。

检查当前环境是否允许系统包:

python -c "import sys; print(sys.prefix); print(sys.base_prefix)"
grep include-system-site-packages .venv/pyvenv.cfg

如果已经使用了该选项,不能通过简单删除一行配置就保证整个环境恢复干净,因为此前安装到环境中的包、生成的脚本和其他路径配置仍可能存在。更安全的恢复方式是删除并重建:

rm -rf .venv
python3.14 -m venv .venv

Windows:

Remove-Item -Recurse -Force .venv
py -3.14 -m venv .venv

9. 用户级 site-packages-s

除了系统包,Python 还可能存在用户级包目录,例如 Unix 上常见的:

~/.local/lib/python3.14/site-packages

Windows 上则可能位于用户应用数据目录。site 模块会根据运行配置决定是否将用户级包目录加入 sys.path。(docs.python.org)

检查用户包目录:

python -m site

典型输出会包含:

sys.path = [...]
USER_BASE: '/home/alice/.local'
USER_SITE: '/home/alice/.local/lib/python3.14/site-packages'
ENABLE_USER_SITE: True

可以使用 -s 禁止用户级 site-packages

python -s -c "import sys, site; print(site.ENABLE_USER_SITE); print(*sys.path, sep='\n')"

-s 不是修复项目环境的常规方案。它适合诊断:

python -c "import some_package; print(some_package.__file__)"
python -s -c "import some_package; print(some_package.__file__)"

如果第一条成功、第二条失败,说明包可能来自用户级目录。

Python 3.14 有一个重要变化:在虚拟环境中,sys.prefixsys.exec_prefix 的设置已经不再依赖 site 模块,因此即使使用 -S 禁用 site 初始化,前缀仍会正确反映虚拟环境。(docs.python.org)

这并不意味着 -S 适合日常运行。-S 会跳过 site 初始化,从而影响 site-packages.pth、用户定制和其他启动行为;它是诊断参数,不是替代虚拟环境的隔离机制。


10. .pth 文件:最隐蔽的路径污染来源之一

site-packages 中,名字形如:

something.pth

的文件可以:

  1. 添加额外目录到 sys.path
  2. 执行以 import 开头的代码行。

例如,某个 .pth 文件可能包含:

/home/alice/old-project/src

于是即使当前虚拟环境没有安装某个包,Python 仍可能从旧项目目录导入它。

更危险的是:

import startup_hook

.pth 中的可执行行会在每次 Python 启动时处理,而不要求程序真的导入某个业务模块。Python 官方文档明确警告,.pth 中的可执行行会在每次启动执行,因此应尽量保持影响最小。(docs.python.org)

列出当前环境中的 .pth 文件:

python -c "import sysconfig, pathlib; p=pathlib.Path(sysconfig.get_path('purelib')); print(p); print(*p.glob('*.pth'), sep='\n')"

查看内容:

find .venv -name '*.pth' -type f -print -exec sed -n '1,120p' {} \;

Windows PowerShell:

Get-ChildItem .venv -Filter *.pth -Recurse | ForEach-Object {
    $_.FullName
    Get-Content $_.FullName
}

.pth 文件并不天然有害。某些安装机制会使用它们处理路径或导入钩子;问题在于项目不应无意中依赖指向旧工作区、临时目录或个人目录的路径。


11. 可编辑安装产生的“看似未安装”状态

开发项目经常使用:

python -m pip install -e .

这称为可编辑安装。它的目标不是把源码完整复制到 site-packages,而是让环境中的导入路径指向当前源码目录,使源码修改可以立即生效。

因此,下面两件事可能同时成立:

.venv/lib/python3.14/site-packages/ 中看不到完整的项目源码
import myproject 成功

诊断时应同时检查:

python -m pip show myproject
python -c "import myproject; print(myproject.__file__)"

如果 myproject.__file__ 指向当前源码目录,这是可编辑安装的正常结果;如果指向另一个工作区,则可能是旧的可编辑安装、.pth 或错误解释器造成的污染。

查看已安装发行包:

python -m pip list

查看某个包对应的实际导入位置:

python -c "import importlib.util; spec=importlib.util.find_spec('myproject'); print(spec); print(spec.origin if spec else None)"

这里的因果链是:

pip install -e .
    ↓
环境记录项目元数据
    ↓
导入系统获得源码目录路径
    ↓
import 使用工作区源码,而不是复制后的源码

当工作区被移动、删除或切换分支后,旧的可编辑安装可能继续存在,但指向无效或错误的位置。重建环境可以清除这类残留。


12. 常见污染模式与失败表现

12.1 pippython 不属于同一个环境

错误操作:

pip install flask
python app.py

诊断:

command -v pip
command -v python
pip --version
python -c "import sys; print(sys.executable)"

如果两组路径不一致,改用:

python -m pip install flask

12.2 当前 shell 曾激活过旧环境

例如先进入项目 A:

source ~/projects/a/.venv/bin/activate

随后切换到项目 B,却忘记重新激活。提示符可能仍显示:

(.venv)

诊断不能只看提示符:

python -c "import sys, os; print(sys.executable); print(os.environ.get('VIRTUAL_ENV'))"

如果 sys.executable 指向项目 A,应执行:

deactivate
source ~/projects/b/.venv/bin/activate

或者绕过 shell 状态:

~/projects/b/.venv/bin/python -m pip install -r requirements.txt

12.3 PYTHONPATH 引入共享目录

表现:

  • 包没有安装到当前环境,却可以导入;
  • 修改共享目录中的源码后,项目行为变化;
  • CI、容器和本地结果不一致。

诊断:

python -c "import os, sys; print(os.environ.get('PYTHONPATH')); print(*sys.path, sep='\n')"

12.4 --system-site-packages 隐式提供依赖

表现:

python -m pip show some-package
# 当前环境没有显示
python -c "import some_package"
# 却能成功

此时要检查:

grep include-system-site-packages .venv/pyvenv.cfg
python -c "import some_package; print(some_package.__file__)"

如果导入位置在系统 Python 目录,当前环境并不自洽。

12.5 项目目录遮蔽标准库或第三方包

例如项目中存在:

logging.py
typing.py
email/
json.py

导入路径首项通常包含脚本目录或当前目录,因此这些名称可能遮蔽标准库模块。

诊断:

python -c "import json; print(json.__file__)"

如果输出项目路径而不是 Python 标准库路径,就发生了名称遮蔽。

12.6 编辑器使用了另一个解释器

终端中执行:

python -c "import sys; print(sys.executable)"

输出 .venv/bin/python,但编辑器运行测试时使用:

/usr/bin/python3

那么终端验证不能代表编辑器验证。应在编辑器的运行配置中明确设置解释器路径,并在测试命令中打印:

import sys
print(sys.executable)

12.7 直接运行带 shebang 的旧脚本

某个脚本可能以如下方式开头:

#!/home/alice/old-project/.venv/bin/python

即使当前 shell 已激活新环境,直接执行该脚本:

./tool

仍可能调用旧环境,因为 shebang 是脚本自身的解释器选择。

诊断:

head -n 1 ./tool

必要时重新安装生成脚本,或明确使用当前解释器:

python -m package.module

13. 运行时检查:不要只检查“能不能 import”

“能导入”只证明某个路径上存在可导入对象,并不证明它来自正确环境。

可以建立一个诊断脚本:

from __future__ import annotations

import importlib.metadata as metadata
import importlib.util
import os
import site
import sys
import sysconfig
from pathlib import Path


def print_module(name: str) -> None:
    spec = importlib.util.find_spec(name)
    print(f"{name}:")
    print(f"  spec   = {spec}")
    print(f"  origin = {spec.origin if spec else None}")


print("=== interpreter ===")
print("executable:", sys.executable)
print("version:", sys.version)
print("prefix:", sys.prefix)
print("base_prefix:", sys.base_prefix)
print("in_venv:", sys.prefix != sys.base_prefix)

print("\n=== environment variables ===")
print("VIRTUAL_ENV:", os.environ.get("VIRTUAL_ENV"))
print("PYTHONPATH:", os.environ.get("PYTHONPATH"))

print("\n=== installation paths ===")
print("purelib:", sysconfig.get_path("purelib"))
print("platlib:", sysconfig.get_path("platlib"))
print("scripts:", sysconfig.get_path("scripts"))

print("\n=== site ===")
print("ENABLE_USER_SITE:", site.ENABLE_USER_SITE)
print("USER_SITE:", site.getusersitepackages())
print("GLOBAL_SITE:", site.getsitepackages())

print("\n=== sys.path ===")
for index, path in enumerate(sys.path):
    print(index, repr(path))

print("\n=== selected modules ===")
print_module("json")
print_module("pip")

print("\n=== selected distributions ===")
for name in ("pip", "setuptools"):
    try:
        print(name, metadata.version(name))
    except metadata.PackageNotFoundError:
        print(name, "<not installed>")

运行:

python diagnose.py

重点观察:

  1. sys.executable 是否位于目标 .venv
  2. sys.prefix 是否不同于 sys.base_prefix
  3. purelibplatlib 是否位于目标 .venv
  4. sys.path 中是否出现旧项目目录、用户目录或意外系统目录;
  5. json 等标准库模块是否来自正确的标准库位置;
  6. VIRTUAL_ENV 是否与 sys.executable 所属环境一致。

一个环境是否“干净”,可以形式化为:

dDruntime,d{SB,PE,Rproject}\forall d \in D_{\text{runtime}},\quad d \in \{S_B, P_E, R_{\text{project}}\}

其中:

  • DruntimeD_{\text{runtime}}:运行时实际参与导入的目录;
  • SBS_B:预期基础 Python 标准库目录;
  • PEP_E:目标虚拟环境包目录;
  • RprojectR_{\text{project}}:项目自身源码目录。

如果额外出现:

  • 旧工作区;
  • 未声明的用户包目录;
  • 非预期系统包目录;
  • 临时构建目录;
  • 与当前项目无关的 .pth 路径;

那么环境就存在隐式输入。


14. 如何建立一个可验证的 Python 3.14 环境

下面是一条完整流程。

14.1 选择并验证基础解释器

python3.14 --version
python3.14 -c "import sys; print(sys.executable)"

必须先确认这里确实是目标 Python 3.14,而不是名称相似的其他解释器。

14.2 创建环境

python3.14 -m venv .venv

如果希望创建时更新虚拟环境中的核心依赖,可以使用:

python3.14 -m venv --upgrade-deps .venv

--upgrade-deps 会升级核心依赖;它并不等于安装项目依赖,也不等于生成锁文件。Python 3.14 文档将该选项描述为升级核心依赖 pip。(docs.python.org)

14.3 不依赖激活进行验证

.venv/bin/python --version
.venv/bin/python -c "import sys; print(sys.executable); print(sys.prefix != sys.base_prefix)"
.venv/bin/python -m pip --version

Windows:

.\.venv\Scripts\python.exe --version
.\.venv\Scripts\python.exe -c "import sys; print(sys.executable); print(sys.prefix != sys.base_prefix)"
.\.venv\Scripts\python.exe -m pip --version

预期:

Python 3.14.x
.../.venv/bin/python
True
pip ... from .../.venv/.../site-packages/pip

14.4 安装并核对包的来源

.venv/bin/python -m pip install requests
.venv/bin/python -m pip show requests
.venv/bin/python -c "import requests; print(requests.__version__); print(requests.__file__)"

这里要验证的是:

发行包元数据位置 = 虚拟环境 site-packages
实际导入文件位置 = 虚拟环境 site-packages

如果两者不一致,应先诊断路径,而不是继续安装更多包。

14.5 记录依赖,但不要把虚拟环境当作依赖清单

虚拟环境目录包含安装结果,不是项目的可读依赖声明。项目仍需使用声明文件或项目元数据记录依赖,例如 pyproject.toml

[project]
name = "example-app"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
    "requests>=2.32,<3",
]

版本约束描述“允许哪些版本”,而虚拟环境只描述“当前这次安装实际得到了什么”。锁定、哈希和供应链完整性属于依赖解析与安装策略,不能由 venv 自动提供。PyPA 的规范列表将依赖说明、安装元数据和可复现环境分别作为不同规范领域。(packaging.python.org)


15. venv 与依赖解析、锁定的边界

可以把工程输入分成三层:

第一层:解释器选择

Python 3.14.x

它决定:

  • 字节码和语言特性;
  • 标准库版本;
  • ABI 和平台兼容性;
  • 可接受的包版本范围。

第二层:依赖约束

例如:

requests >= 2.32, < 3

它只描述允许集合:

Crequests={v2.32v<3}C_{\text{requests}} = \{v \mid 2.32 \le v < 3\}

第三层:具体解析结果

解析器还需要考虑:

  • 直接依赖;
  • 间接依赖;
  • Python 版本;
  • 操作系统;
  • CPU 架构;
  • environment markers;
  • wheel 和源码包可用性;
  • 额外依赖 extras;
  • 已有环境中的安装状态。

最终安装的是某个具体解:

R={requests=2.32.5, urllib3=2.5.0, certifi=2025.x}R = \{ \text{requests}=2.32.5,\ \text{urllib3}=2.5.0,\ \text{certifi}=2025.x \}

venv 只提供一个安装边界 PEP_E,并不负责定义 CC,也不负责求解 RR

因此:

python3.14 -m venv .venv

不能保证:

  • 所有机器安装到相同版本;
  • 所有包都来自同一个索引;
  • 下载文件具有相同哈希;
  • 间接依赖不会随时间变化;
  • 依赖解析结果可重复。

这些问题应由项目的依赖声明、锁定格式、索引配置和安装校验共同处理。


16. 为什么虚拟环境通常应该删除重建

虚拟环境常被误解为“可以持续修补的开发机器”。实际上,它更接近一个由输入生成的构建产物:

E=f(I,V,P,O,A)E = f(I, V, P, O, A)

其中:

  • II:解释器版本;
  • VV:依赖版本和约束;
  • PP:平台;
  • OO:操作系统;
  • AA:架构和 ABI;
  • EE:生成的虚拟环境。

当环境经历很多次:

pip install ...
pip uninstall ...
pip install -U ...
pip install -e .

之后,环境中可能出现:

  • 不再被项目需要的包;
  • 旧版本残留;
  • 旧的可编辑安装;
  • .pth 文件;
  • 依赖树与当前声明不一致;
  • 被手工复制进去的文件;
  • 与当前解释器不匹配的编译扩展。

此时继续补丁式修复的成本可能高于重建:

rm -rf .venv
python3.14 -m venv .venv
.venv/bin/python -m pip install ...

删除重建并不自动产生可复现性。它只是移除了历史状态;要获得可复现环境,还需要稳定的依赖声明、解析结果和安装来源。


17. 生产环境中的取舍

17.1 应用部署

对于应用,常见做法是:

  1. 在明确版本的 Python 3.14 基础上创建环境;
  2. 安装经过解析和验证的依赖;
  3. 运行时直接调用环境中的解释器或入口脚本;
  4. 不把 .venv 提交到源码仓库;
  5. 不依赖开发者机器上的用户包和系统包。

生产中不一定必须使用 venv。容器、系统包管理器或专用部署工具也可以提供环境边界。但无论使用什么方式,都应明确:

哪个解释器?
哪些包?
从哪里下载?
允许哪些版本?
最终使用哪些文件?

17.2 Python 库开发

对于库项目,虚拟环境用于:

  • 测试某个 Python 版本;
  • 测试不同依赖组合;
  • 执行构建和检查工具;
  • 避免污染开发者的全局 Python。

库本身不应把 .venv 打包发布,也不应假设用户会使用与你相同路径的虚拟环境。库应该通过项目元数据声明运行时依赖和 Python 版本范围。

17.3 --system-site-packages 的合理边界

该选项有时用于:

  • 系统提供大型二进制库;
  • 受限机器不允许重复安装依赖;
  • 发行版希望让系统包管理器提供 Python 包。

但它牺牲了环境自洽性。只要项目需要跨机器、进入 CI、构建容器或进行供应链审计,就应谨慎使用,且必须把系统包视为显式外部输入,而不是“免费可用”的依赖。


18. 一组最小但有效的排查命令

遇到“明明安装了却无法导入”时,按以下顺序执行:

python -c "import sys; print(sys.executable)"
python -m pip --version
python -c "import sys; print(sys.prefix); print(sys.base_prefix)"
python -c "import sys; print(*sys.path, sep='\n')"
python -m site
python -m pip show 包名
python -c "import importlib.util; print(importlib.util.find_spec('导入名'))"

判断逻辑如下:

  1. sys.executable 错了:先修正解释器选择;
  2. python -m pip --version 路径错了:不要继续安装,先修正环境;
  3. sys.prefix == sys.base_prefix:当前不是虚拟环境;
  4. sys.path 出现旧目录:检查 PYTHONPATH、当前目录和 .pth
  5. pip show 找不到但 import 成功:检查系统包、用户包和路径注入;
  6. pip show 能找到但 import 失败:安装名和导入名可能不同,或安装不完整;
  7. 导入文件来自错误目录:检查名称遮蔽、编辑器解释器和旧可编辑安装。

最重要的诊断原则是:

先确认“哪个解释器在运行”,再确认“它从哪些路径导入”,最后才检查“包是否安装”。

虚拟环境的核心不是提示符中的括号,也不是目录名 .venv,而是解释器、前缀、安装目录和 sys.path 之间形成的一组可验证关系。


系列导航与关联阅读

官方资料

本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。