Python: как грамотно работать с версиями пакетов

Раздел: Управление пакетами -> Версии пакетов

Работа с версиями пакетов в Python - одна из ключевых задач управления зависимостями. Без proper указания версий проект может перестать работать при обновлении библиотек или при переносе на другую машину. В этой статье разбираются способы фиксации, проверки и гибкого задания версий, а также типовые ошибки и их решения.

Основные подходы к указанию версий пакетов

Как надёжно зафиксировать версии всех используемых пакетов в проекте?

Самый распространённый и эффективный способ - использование файла requirements.txt в связке с командой pip freeze. После установки всех нужных пакетов в виртуальное окружение выполняется:

pip freeze > requirements.txt

Python версия пакета (версия пакета в python)

В результате создаётся файл с точными номерами версий, например:

requests==2.31.0
numpy==1.24.3
flask==2.3.2

При развёртывании проекта зависимости устанавливаются командой:

pip install -r requirements.txt

Типичная проблема:

Если при выполнении pip freeze в окружении присутствуют лишние пакеты, они тоже попадут в файл. Решение - предварительно создать чистое виртуальное окружение и установить только необходимые зависимости.

Цель: получить воспроизводимое окружение для всех разработчиков и серверов. Подходит для небольших проектов или когда нет сложной системы управления зависимостями.

Как указать диапазон версий в requirements.txt?

Иногда требуется не точная версия, а совместимый диапазон. Используются операторы:

  • == - точное совпадение;
  • >= - не ниже указанной;
  • <= - не выше;
  • != - исключение версии;
  • ~= - совместимая версия (аналог ~= в семантическом версионировании).

Пример:

requests>=2.28.0
numpy>=1.23,<1.25
flask~=2.3

Важно: оператор ~= для пакета flask~=2.3 означает flask>=2.3, <2.4, то есть разрешает только минорные обновления.

Ошибка:

Неправильное понимание ~= может привести к установке неожиданной версии. Например, numpy~=1.24 даст numpy>=1.24,<1.25, но если нужно 1.24.* - это верно. А numpy~=1.24.0 даст >=1.24.0,<1.25 - разница существенна.

Когда использовать: если проект не требует жёсткой фиксации и допустимы минорные исправления.

Как зафиксировать версии в pyproject.toml (PEP 621)?

Современные инструменты (Poetry, Flit, PDM) используют файл pyproject.toml для описания зависимостей. Пример для Poetry:

[tool.poetry.dependencies]
python = "^3.9"
requests = "^2.31.0"
numpy = "*"

Символ ^ означает «совместимую» версию: ^2.31.0 разрешает >=2.31.0, <3.0.0. Точный номер фиксируется в poetry.lock.

Для установки всех зависимостей:

poetry install

Проблема:

При переносе проекта без файла блокировки poetry.lock может установиться другая версия, если используются диапазоны. Всегда нужно коммитить lock-файл.

Когда лучше pyproject.toml: для библиотек и приложений, где требуется строгое управление зависимостями, включая разделение на основные и dev-пакеты.

Как узнать текущую установленную версию пакета?

Из командной строки:

pip show requests

или для всех пакетов:

pip list

В коде Python (начиная с Python 3.8) можно использовать importlib.metadata:

from importlib.metadata import version
try:
    ver = version("requests")
    print(ver)
except PackageNotFoundError:
    print("Пакет не установлен")

Результат работы (если requests установлен):

2.31.0

Ошибка:

Если пакет не найден, возникнет PackageNotFoundError. Следует обрабатывать исключение.

Цель: динамически проверять версию внутри скрипта - полезно для совместимости.

Как установить конкретную версию из нестандартного источника (git, локальный архив)?

Из git-репозитория:

pip install git+https://github.com/psf/requests.git@v2.31.0

Из локального файла (.whl или .tar.gz):

pip install ./downloads/requests-2.31.0-py3-none-any.whl

В requirements.txt это записывается так:

requests @ git+https://github.com/psf/requests.git@v2.31.0
numpy @ file:///path/to/numpy-1.24.3.tar.gz

Проблема:

При использовании @-синтаксиса pip может потребовать предварительную установку дополнительных инструментов (git). Также файл не должен перемещаться.

Когда пригодится: если пакета нет в PyPI или нужна разработочная версия с патчем.

Расширенные примеры работы с версиями пакетов

Ниже представлены менее распространённые, но полезные сценарии управления версиями.

1. Создание файла constraints.txt для ограничений

Файл constraints позволяет задать версии, которые pip будет учитывать, но не устанавливать напрямую. Полезен для монорепозитория или множества requirements-файлов.

Содержимое constraints.txt:

Пример
requests>=2.28.0,<3.0.0
numpy!=1.24.2

Использование:

Пример
pip install -c constraints.txt somepackage

Результат: pip не установит requests версии ниже 2.28 и выше 3.0, а numpy версии 1.24.2 будет исключён.

2. Использование pip-compile (из pip-tools) для генерации lock-файла

Утилита pip-compile принимает верхнеуровневые зависимости и создаёт requirements.txt с полным деревом версий.

Файл requirements.in:

Пример
flask
requests

Команда:

Пример
pip-compile requirements.in

Создаётся requirements.txt с точными версиями всех подзависимостей:

Пример
# This file is autogenerated by pip-compile
click==8.1.3
flask==2.3.2
itsdangerous==2.1.2
requests==2.31.0
...

Проблемы: pip-compile требует установленного пакета pip-tools. Lock-файл может устареть при отключении от сети.

3. Установка версии, указанной в самом пакете (метаданные)

Иногда нужно прочитать версию, которая указана в setup.py или pyproject.toml пакета перед его установкой. Пример для локального пакета:

Пример
import re
with open("setup.cfg") as f:
    content = f.read()
version = re.search(r"version\s*=\s*([^\s]+)", content).group(1)
print(version)

Результат (при наличии строки version = 0.1.0):

0.1.0

Такой способ не рекомендуется для общего использования, но может пригодиться в скриптах сборки.

4. Проверка совместимости версий с помощью семантического версионирования

Библиотека packaging позволяет программно сравнивать версии:

Пример
from packaging.version import Version
from packaging.specifiers import SpecifierSet

v1 = Version("2.1.0")
spec = SpecifierSet(">=2.0.0, <3.0.0")
print(spec.contains(v1))  # True

# Проверка совместимости по символу ~=
v2 = Version("2.5.0")
spec_tilde = SpecifierSet("~=2.3")
print(spec_tilde.contains(v2))  # True (2.5 >= 2.3, < 2.4? Нет! Здесь ошибка)
True
False  # На самом деле ~=2.3 даёт SpecifierSet('>=2.3, <2.4'), поэтому 2.5 не входит

Пояснение: ~= ограничивает только последнюю цифру. Для ~=2.3 допустимы версии от 2.3.0 до 2.3.*, а не любые 2.x. Правильная запись для мажорной совместимости - >=2.0,<3.0 или ^2.0 в Poetry.

5. Одновременная установка двух версий одного пакета (не рекомендуется)

Обычно pip не позволяет этого, но для экспериментов можно использовать изолированные окружения или прием с переименованием. Например, установить пакет в каталог с другим именем через pip -target:

Пример
pip install --target ./requests_v1 requests==1.0
pip install --target ./requests_v2 requests==2.31.0

В коде нужно добавлять пути в sys.path. Это крайне нестандартный подход, создающий путаницу.

6. Использование environment markers для условных версий

В requirements.txt можно указывать зависимости, актуальные только для определённых платформ или версий Python:

Пример
numpy; python_version < "3.10"
pywin32; sys_platform == "win32"

При установке pip учтёт эти маркеры и подставит версию из Pypi (или из указанного диапазона).

Результат на Linux:

Collecting pywin32
  Skipped: pywin32 is not supported on this platform (linux)

Возможная проблема: если маркер написан с синтаксической ошибкой, pip проигнорирует строку.

7. Заморозка версий с помощью pipenv lock

Альтернатива poetry - pipenv. Создаёт Pipfile и Pipfile.lock:

Пример
pipenv install requests==2.31.0
pipenv lock  # обновляет lock-файл

После этого на другой машине:

Пример
pipenv sync  # устанавливает версии из lock-файла

Отличие от pip freeze: pipenv замораживает не только прямые зависимости, но и все транзитивные, а также хеши пакетов для проверки целостности.

Версия пакета в Python - comments

En
Python версия пакета (python)