Deep Engineering

MEASUREMENT

bench/venv/practice.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/interview/python/venv-dependencies
How to run it
python3.11 bench/venv/basics.py
python3.12 bench/venv/basics.py
python3.13 bench/venv/basics.py
python3.14 bench/venv/basics.py

python3.11 bench/venv/practice.py
python3.12 bench/venv/practice.py
python3.13 bench/venv/practice.py
python3.14 bench/venv/practice.py

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Замеры для урока «Окружения и зависимости»

Скрипт Что делает
basics.py создаёт окружение во временной папке и смотрит на него: что в папке и в pyvenv.cfg, sys.prefix без активации, пакеты в свежем окружении, изоляция поставленного пакета и pip freeze, что меняет activate, что ломается в копии окружения, что пишет pip freeze для пакетов из PyPI, из локальной папки и editable, и что видит env/bin/python -S
practice.py источник ответов двух практических задач урока
python3.11 bench/venv/basics.py
python3.12 bench/venv/basics.py
python3.13 bench/venv/basics.py
python3.14 bench/venv/basics.py

python3.11 bench/venv/practice.py
python3.12 bench/venv/practice.py
python3.13 bench/venv/practice.py
python3.14 bench/venv/practice.py

Запускать самим интерпретатором, а не символической ссылкой на него: venv определяет базовую установку по пути интерпретатора. Раздел 4 basics.py ставит tomli-w==1.0.0 из PyPI, раздел 7 скачивает setuptools для сборки двух локальных пакетов — нужна сеть.

Записи в runs/: без суффикса — CPython 3.13.7, остальные — с суффиксом версии (3.11.15, 3.12.3, 3.14.7). Путь временной папки в выводе заменён на <папка>, путь базовой установки — на <python>.

Что получилось

Папка окружения: bin/, include/, lib/, lib64 (символическая ссылка на lib), pyvenv.cfg; с 3.13 — ещё .gitignore (его создаёт командная строка python -m venv). bin/python — символическая ссылка на базовый интерпретатор; абсолютный путь окружения записан в bin/activate и в первой строке bin/pip (#!<папка>/env/bin/python3.13). На 3.14 в bin/ есть ещё имя 𝜋thon.

Без активации: sys.prefix — папка окружения, sys.base_prefix — базовая установка, пакеты ставятся в site-packages окружения.

Свежее окружение: 3.11 — pip==24.0 и setuptools==79.0.1; 3.12 — pip==24.0; 3.13 — pip==25.2; 3.14 — pip==26.2.1. Наполняет его ensurepip; набор пакетов задан в Lib/ensurepip/__init__.py каждой версии.

Изоляция: tomli_w импортируется интерпретатором окружения и не импортируется базовым (ModuleNotFoundError). У базового sys.prefix — <python>, пакеты — в <python>/lib/python3.13/site-packages (у сборок 3.11 и 3.12 из Ubuntu — <python>/local/lib/python3.1X/dist-packages). В метаданных пакета Name: tomli_w; pip freeze — tomli_w==1.0.0, без самого pip (на 3.11 — и без setuptools).

activate: ставит env/bin первым в PATH, задаёт VIRTUAL_ENV и VIRTUAL_ENV_PROMPT (env на 3.13 и 3.14, (env) на 3.11 и 3.12); deactivate возвращает PATH прежним. Что ещё делает шаблон activate — приглашение PS1, снятие PYTHONHOME — прогон не печатает.

Копия в другую папку: moved/bin/python ставит sys.prefix на <папка>/moved, а moved/bin/pip по шебангу работает со старым окружением (<папка>/env/lib/…/pip); moved/bin/python -m pip — с копией. Когда старой папки нет, moved/bin/pip не запускается: FileNotFoundError. Одинаково на 3.11–3.14.

pip freeze (раздел 7): снимок в формате requirements — tomli_w==1.0.0 для пакета из PyPI, localpkg @ file://<папка>/localpkg для пакета из локальной папки, # Editable install with no version control (editpkg==0.1) и -e <папка>/editpkg для editable. --all добавляет то, что freeze пропускает: на 3.11 — pip==24.0 и setuptools==79.0.1, на 3.12–3.14 — только pip. Правило пропуска — pip/_internal/commands/freeze.py (pip 24.0: строки 12–20, по sys.version_info < (3, 12) к pip добавляются setuptools, distribute, wheel).

env/bin/python -S (раздел 8): на 3.11–3.13 sys.prefix — <python>, prefix != base_prefix — False; на 3.14 — <папка>/env и True. С 3.14 префиксы окружения выставляет инициализация путей, а не site (документация site 3.14, «Changed in version 3.14»). site-packages окружения в sys.path под -S нет ни на одной версии.

Script

43 lines
"""Практика урока «Окружения и зависимости»: источник ответов двух задач.

Ответ задачи не сочиняется редакцией — он берётся из записи прогона этого
скрипта (`runs/practice.txt`), а сборка сверяет одно с другим
(`scripts/validate-practice.mjs`).

ЧТО ПРОВЕРЯЮТ ЗАДАЧИ. Первая — что интерпретатор окружения знает, что он в
окружении, и без активации: код задачи выполняется через env/bin/python,
activate не вызывается. Вторая — сколько пакетов лежит в свежем окружении.

Запускать: python3.11 / 3.12 / 3.13 / 3.14 (самим интерпретатором, не
символической ссылкой на него).
"""
import os
import subprocess
import sys
import tempfile

print("PY", sys.version.split()[0])
print()

tmp = tempfile.mkdtemp()
env_dir = os.path.join(tmp, "env")
subprocess.run([sys.executable, "-m", "venv", env_dir], check=True)
env_py = os.path.join(env_dir, "bin", "python")

TASK = """
import sys
print(sys.prefix == sys.base_prefix)
print(sys.prefix.endswith("env"))
"""

print("вывод задачи:")
print(subprocess.run([env_py, "-c", TASK], capture_output=True, text=True, check=True).stdout.rstrip())

print()
print("оценка:")
listed = subprocess.run(
    [env_py, "-m", "pip", "list", "--format=freeze", "--disable-pip-version-check"],
    capture_output=True, text=True, check=True,
).stdout.split()
print("пакетов в свежем окружении:", len(listed))