Python 项目为什么要使用虚拟环境

三年前我电脑里两个项目共用全局 Python,装一个 httpx 0.22 直接把 Django 那边搞崩。从那天起每个项目必须有自己的环境。这篇是我现在的工作流,外加几个真实翻车:语法版本差异、python 命令找不到、rebuild 后忘装依赖。
我跟 A 项目的车祸
三年前我电脑里同时装着两个项目。一个叫 A,跑 FastAPI 0.78;另一个叫 B,是朋友介绍的小工具,他让我顺手用 Django 2.2 改一改。
我有天想安装 A 项目需要的一个新依赖 httpx 0.22,pip install httpx==0.22 一执行,Django 那边立刻炸了。原因:A 项目里有一个老包只能用 Django 2.x 的某个子版本,httpx 0.22 升级把它的依赖 transitive 关系搅了。
那一天我学到的不只是怎么修依赖,而是一套原则:全局 Python 环境是公共厕所,谁来踩一脚都不奇怪。我需要的是每个项目有自己的「房间」。
创建最简单的虚拟环境
python -m venv .venv执行完会创建 .venv/ 目录,里面有 bin/python、lib/python3.13/、include/。从这天起,只要进入这个目录里执行 python,pip 安装的所有包都会进 .venv/,跟系统 Python 完全隔离。
激活它:
source .venv/bin/activate激活后命令行前缀会变成 (.venv) ,提醒我现在的 python 指向的是 .venv/bin/python,不是 /usr/bin/python。
关闭它:
deactivate或者直接关掉 shell。
我自己规范下来的几条工作习惯
只用 python -m venv。其它创建虚拟环境的工具(比如 virtualenv、pipenv、poetry、uv)我用过一阵,最终都回到了 venv。理由:它是 Python 自带的,零依赖,三行命令创建,对老 Python 版本(3.8 起)都支持。
虚拟环境永远建在项目根目录下,命名为 .venv。Git 会自动忽略它,前置的点让它在 ls 时排在最前,肉眼能找到。
requirements.txt 与 requirements-dev.txt 分开。我博客项目里 production 依赖是 FastAPI、uvicorn、httpx;开发依赖是 pytest、black、ruff。线上 pip install -r requirements.txt,本地加 -r requirements-dev.txt。
锁文件 vs 锁依赖版本
requirements.txt 之前我说过记录依赖即可。但团队项目里需要锁住确切版本,否则 A 机器跟 B 机器拉到的子依赖可能有微小差异。
我现在的实践是 requirements.txt 写直接依赖的最低版本,requirements-lock.txt(pip freeze 输出)锁住所有子依赖:
# requirements.txt
fastapi>=0.115,<1
uvicorn[standard]>=0.30,<1
python-multipart>=0.0.9# requirements-lock.txt(自动生成,不手改)
annotated-types==0.7.0
anyio==4.6.0
fastapi==0.115.6
pydantic==2.10.3
...CI 跑测试时用 pip install -r requirements-lock.txt,确保所有人跑同样的环境。本地开发用 requirements.txt,新加依赖后跑 pip freeze > requirements-lock.txt 重新出锁。
一个真实翻车:Python 3.12 vs 3.10 的语法差异
有次我把一个本地跑的好好的脚本丢到同事机器上,他装的是 Python 3.10。报错里 match-case 不识别。我用的是 Python 3.12 才稳定下来的一些语法糖。
教训:requirements.txt 锁不住 Python 解释器版本。锁的是「用什么 Python 解释器跑」和「装哪些包」。
我现在的做法是写一份 runtime.txt:
python-3.13.12CI 流水线用这个版本,团队文档里写明,最低 Python 版本是 3.10(drop 了 3.9 是因为 3.10 有一些匹配模式我开始用)。
老项目偶遇 bash: python: command not found
记一次小白错误。第一次用 venv 时创建了 .venv,但跑 python 找不到,因为我没激活它。需要在 .venv/bin/ 下手动调用:
./.venv/bin/python --version
./.venv/bin/pip install ...后来我写了个 shell 函数:
function py() {
if [ -d ".venv" ]; then
.venv/bin/python "$@"
else
echo "no .venv in current dir"
return 1
fi
}这样无论是否激活,直接敲 py 就行。
.venv 不进 git
.venv/ 体积大(几百 MB)、跟操作系统相关、跟 Python 版本相关,但跟项目逻辑没关系。仓库里看到 .gitignore:
.venv/
__pycache__/
*.pyc
*.pyo新同事 clone 之后:「我执行 python -m venv .venv && .venv/bin/pip install -r requirements-lock.txt 就跑起来了」。README.md 里要写这两行。任何不写这两行的 Python 项目都不专业。
一个我自己翻车过的坑:rebuild 之后忘了装依赖
我刚接手某个 AI 项目时,clone 下来直接跑 python main.py,报 ModuleNotFoundError: openai。我看了下项目根目录,有 .venv/ 但里面的依赖早被覆盖了。
那次我学会一个动作:项目重建,先把 venv 删了重建。这样比诊断老环境稳得多:
rm -rf .venv
python -m venv .venv
.venv/bin/pip install -r requirements-lock.txt
.venv/bin/python main.py重建可能会浪费时间,但比调试「明明我装了为什么还在」节省总时间。
我现在标准工作流
git clone <project-url>
cd project-folder
python -m venv .venv
.venv/bin/pip install -r requirements-lock.txt
.venv/bin/pip install -r requirements-dev.txt # 可选
.venv/bin/pytest # 跑测试看是否能起来
.venv/bin/python -m uvicorn app.main:app --reload # 起开发服务这套几乎能 cover 所有 Python 项目的起步。当团队新人入职,一份 README 配上这些命令他就能走通。