Как задать версию пакета с помощью __init__.py

Раздел: Основы Python -> Пакеты и модули

Файл __init__.py и версионирование пакетов

Как эффективно задать версию пакета в __init__.py и обеспечить её доступность как внутри пакета, так и для внешних инструментов?

Наиболее эффективное решение сочетает ручное определение атрибута __version__ в файле __init__.py с возможностью получения версии из метаданных установленного пакета через importlib.metadata. Такой подход даёт единую точку версии во время разработки и корректное чтение версии после установки пакета.

Пример реализации:

# mypackage/__init__.py
__version__ = '1.2.3'

# Для получения версии после установки пакета (например, в другом модуле)
from importlib.metadata import version

def get_package_version():
    try:
        return version('mypackage')
    except:
        return __version__

Python init py version (файл __init__.py и версионирование)

>>> import mypackage
>>> mypackage.__version__
'1.2.3'
>>> mypackage.get_package_version()
'1.2.3'

Цель: обеспечить доступ к версии пакета без дополнительной установки (через атрибут __version__) и при установленном пакете (через метаданные, которые обновляются автоматически при публикации). Случай использования: разработка пакета с открытым исходным кодом, где требуется версионирование для CI/CD и для пользователей.

Типичная ошибка: несоответствие значения __version__ в __init__.py и версии, указанной в setup.cfg или pyproject.toml. При публикации на PyPI версия из метаданных становится приоритетной, и если они различаются, пользователи могут получить неактуальную информацию. Решение: использовать единый источник версии (например, динамическое чтение из __init__.py при сборке или инструменты автоматической синхронизации).

Как получить версию только из метаданных, не дублируя её в коде?

Можно полностью отказаться от атрибута __version__ и полагаться только на importlib.metadata. Это устраняет дублирование, но версия становится доступна только после установки пакета. В процессе разработки (при запуске из исходного кода) получить версию таким способом не удастся.

# mypackage/__init__.py
# без __version__
# использование в другом месте
from importlib.metadata import version
print(version('mypackage'))
'1.2.3'

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

Как хранить версию в отдельном файле _version.py и импортировать её в __init__.py?

Отделение версии в собственный модуль упрощает автоматическую генерацию (например, с помощью скриптов или инструментов вроде versioneer).

# mypackage/_version.py
__version__ = '2.0.0'
# mypackage/__init__.py
from ._version import __version__

# теперь __version__ доступен как mypackage.__version__

Цель: изоляция версии для автоматического обновления. Случай: использование versioneer или setuptools-scm, которые генерируют _version.py на основе тегов git.

Как автоматически генерировать версию из системы контроля версий (Git)?

Инструменты setuptools-scm и versioneer позволяют вычислять версию на основе Git-тегов и коммитов. Они создают (или обновляют) файл _version.py или добавляют атрибут в __init__.py.

# pyproject.toml с setuptools-scm
[build-system]
requires = ["setuptools-scm"]
build-backend = "setuptools.build_meta"

[project]
name = "mypackage"
dynamic = ["version"]

[tool.setuptools_scm]
write_to = "mypackage/_version.py"

После сборки в mypackage/_version.py будет сгенерирована строка версии. В __init__.py делается импорт:

from ._version import __version__

Цель: полностью автоматическое версионирование без ручного обновления. Случай: проекты с активной разработкой на Git и регулярными релизами.

Как использовать старый метод pkg_resources для получения версии?

До появления importlib.metadata часто применялся pkg_resources из setuptools. Он медленнее и считается устаревшим, но иногда встречается в легаси-проектах.

import pkg_resources
version = pkg_resources.get_distribution('mypackage').version

Цель: совместимость со старым кодом. Случай: поддержка пакетов, написанных до Python 3.8.

Как хранить версию в текстовом файле (например, VERSION) и читать его в __init__.py?

Некоторые проекты помещают версию в отдельный файл VERSION или version.txt и читают его при импорте. Это позволяет другим инструментам (например, Makefile) легко получить версию.

# VERSION
3.1.4
# mypackage/__init__.py
import os

with open(os.path.join(os.path.dirname(__file__), '..', 'VERSION')) as f:
    __version__ = f.read().strip()

Цель: централизованное хранение версии вне Python-кода. Случай: проекты, где версия нужна не только Python-приложениям (например, для скриптов сборки).

Проблема при использовании файла VERSION: если пакет установлен, относительный путь '..' может не указывать на директорию с файлом VERSION. Это приведет к ошибке FileNotFoundError. Решение: использовать importlib.resources для чтения файла внутри пакета, либо копировать VERSION в пакет при сборке.

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

Иерархическое версионирование подпакетов

В больших пакетах можно задавать версию для каждого подпакета отдельно, экспортируя её через __init__.py.

Пример
# mypackage/subpackage1/__init__.py
__version__ = '1.0.0'
Пример
# mypackage/subpackage2/__init__.py
__version__ = '2.0.0'
Пример
# Использование
import mypackage.subpackage1
print(mypackage.subpackage1.__version__)  # '1.0.0'

Такая структура полезна, если подпакеты развиваются с разной скоростью. Однако следует документировать, что версия корневого пакета (например, mypackage.__version__) может не совпадать с версиями вложенных модулей.

Автоматическое обновление версии с помощью versioneer

versioneer создаёт файл _version.py на основе Git-тегов и может внедрять переменную в __init__.py. Полная настройка включает:

Пример
# Установка и инициализация
pip install versioneer
versioneer install

После этого в setup.cfg добавляется:

Пример
[versioneer]
VCS = git
style = pep440
versionfile_source = mypackage/_version.py
versionfile_build = mypackage/_version.py
tag_prefix = v
parentdir_prefix = mypackage-

В __init__.py:

Пример
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions

Результат при наличии тега v1.2.0:

>>> import mypackage
>>> mypackage.__version__
'1.2.0'

При отсутствии тега версия будет содержать хеш коммита.

Использование __all__ вместе с __version__

В __init__.py можно экспортировать не только версию, но и список публичных имён. Это помогает инструментам статического анализа.

Пример
# mypackage/__init__.py
__version__ = '0.5.0'
__all__ = ['submodule1', 'submodule2', '__version__']

from . import submodule1, submodule2

Теперь from mypackage import * импортирует и __version__, и подмодули.

Динамическое получение версии из git в рантайме (без файла)

Некоторые проекты отказываются от хранения версии в файле и получают её напрямую из Git при запуске. Это не рекомендуется для установленных пакетов, но возможно в среде разработки.

Пример
import subprocess
import os

def get_git_version():
    try:
        return subprocess.check_output(
            ['git', 'describe', '--tags'],
            cwd=os.path.dirname(__file__)
        ).decode().strip()
    except Exception:
        return 'unknown'

__version__ = get_git_version()

Результат:

>>> import mypackage
>>> mypackage.__version__
'v1.0.0-5-gabcdef'

Недостаток: высокая задержка при первом импорте, зависимость от Git и отсутствие версии после установки (если репозиторий не включён).

Кэширование версии при импорте

Если получение версии затратно (например, вызов Git), можно сохранить результат в глобальную переменную.

Пример
# mypackage/__init__.py
_version = None

def _get_version():
    global _version
    if _version is None:
        # вычисляем один раз
        from importlib.metadata import version
        try:
            _version = version('mypackage')
        except:
            _version = '0.0.0.dev'
    return _version

__version__ = _get_version()

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

Файл __init__.py и версионирование - comments

En
Python init py version (python)