Version Build Hook
The version build hook is a powerful feature that automatically writes version information to files during the build process. This ensures packaged code always contains the correct version, regardless of how it's determined (static, dynamic, or from environment variables).
Overview
The version build hook:
- Runs during package building (wheel, sdist)
- Creates or updates files with version information
- Supports templating for custom formats
- Works with all version sources
- Ensures version availability at runtime
Basic Configuration
Enable the version build hook in your pyproject.toml:
[tool.hatch.build.hooks.version]
path = "src/my_package/_version.py"
This creates _version.py during build:
# Automatically generated by Hatch
__version__ = "1.2.3"
Configuration Options
Global Hook Configuration
[tool.hatch.build.hooks.version]
path = "src/my_package/_version.py"
template = '__version__ = "{version}"'
pattern = "__version__ = ['\"](?P<version>[^'\"]+)['\"]"
Target-Specific Configuration
Configure per build target:
# For wheels only
[tool.hatch.build.targets.wheel.hooks.version]
path = "src/my_package/_version.py"
# For source distributions only
[tool.hatch.build.targets.sdist.hooks.version]
path = "src/my_package/_version.py"
Configuration Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
path |
string | File path to write version | Required |
template |
string | Template for file content | __version__ = "{version}" |
pattern |
string/bool | Pattern for updating existing file | true (uses default) |
Template Formatting
Simple Template
Basic version string:
[tool.hatch.build.hooks.version]
path = "src/my_package/_version.py"
template = '__version__ = "{version}"'
Result:
__version__ = "1.2.3"
Multi-line Template
Include additional metadata:
[tool.hatch.build.hooks.version]
path = "src/my_package/_version.py"
template = '''
"""Version information."""
__version__ = "{version}"
__version_tuple__ = {version_tuple}
'''
Result:
"""Version information."""
__version__ = "1.2.3"
__version_tuple__ = (1, 2, 3)
Available Template Variables
| Variable | Description | Example |
|---|---|---|
{version} |
Full version string | "1.2.3" |
{version_tuple} |
Version as tuple | (1, 2, 3) |
Custom Templates
Complex version information:
[tool.hatch.build.hooks.version]
path = "src/my_package/version_info.py"
template = '''
"""
Automatically generated version information.
Do not edit this file manually.
"""
from typing import Tuple
__version__: str = "{version}"
__version_tuple__: Tuple[int, ...] = {version_tuple}
def get_version() -> str:
"""Get the current version."""
return __version__
def get_version_tuple() -> Tuple[int, ...]:
"""Get version as a tuple of integers."""
return __version_tuple__
'''
Pattern-Based Updates
Using Default Pattern
The default pattern matches standard Python version declarations:
[tool.hatch.build.hooks.version]
path = "src/my_package/__init__.py"
pattern = true # Use default pattern
Updates existing version in file:
# Before build
__version__ = "0.0.0" # Placeholder
# After build
__version__ = "1.2.3" # Updated
Custom Pattern
Specify custom regex patterns:
[tool.hatch.build.hooks.version]
path = "setup.cfg"
template = "version = {version}"
pattern = "version = .*"
Updates:
# Before
[metadata]
version = 0.0.0
# After
[metadata]
version = 1.2.3
JSON File Updates
Update version in JSON files:
[tool.hatch.build.hooks.version]
path = "package.json"
template = ' "version": "{version}",'
pattern = ' "version": "[^"]*",'
Integration Patterns
With Dynamic Version Sources
Works seamlessly with all version sources:
[project]
name = "my-package"
dynamic = ["version"]
[tool.hatch.version]
source = "regex"
path = "VERSION"
[tool.hatch.build.hooks.version]
path = "src/my_package/_version.py"
Build process:
- Read version from
VERSIONfile - Write version to
_version.py - Include both in package
With Environment Variables
Perfect for CI/CD:
[tool.hatch.version]
source = "env"
variable = "BUILD_VERSION"
[tool.hatch.build.hooks.version]
path = "src/my_package/_version.py"
BUILD_VERSION=1.2.3 hatch build
# Creates _version.py with version 1.2.3
Multiple Version Files
Create multiple version files:
# Primary version file
[tool.hatch.build.hooks.version]
path = "src/my_package/_version.py"
# Additional version files via custom hook
[tool.hatch.build.hooks.custom]
path = "hatch_build.py"
# hatch_build.py
from pathlib import Path
def initialize(version, build_config):
"""Create additional version files."""
# Create version.txt
version_txt = Path("src/my_package/version.txt")
version_txt.write_text(version)
# Create VERSION file
version_file = Path("VERSION")
version_file.write_text(version)
Runtime Version Access
Method 1: Import Version File
# src/my_package/__init__.py
try:
from ._version import __version__
except ImportError:
__version__ = "unknown"
Method 2: With Fallback
# src/my_package/__init__.py
try:
from ._version import __version__
except ImportError:
# Development mode - version file not created yet
try:
from importlib import metadata
__version__ = metadata.version("my-package")
except Exception:
__version__ = "0.0.0+dev"
Method 3: Version Module
# src/my_package/version.py (created by hook)
__version__ = "1.2.3"
# src/my_package/__init__.py
from .version import __version__
Build Target Specific Hooks
Different Files for Different Targets
# Wheel gets compiled version
[tool.hatch.build.targets.wheel.hooks.version]
path = "src/my_package/_version.py"
template = '__version__ = "{version}"'
# Source distribution gets detailed version
[tool.hatch.build.targets.sdist.hooks.version]
path = "src/my_package/_version_full.py"
template = '''
__version__ = "{version}"
__version_tuple__ = {version_tuple}
__build_date__ = "2024-01-01"
'''
Conditional Hook Activation
# Only for wheel builds
[tool.hatch.build.targets.wheel]
hooks = ["version"]
# No version hook for sdist
[tool.hatch.build.targets.sdist]
hooks = [] # Explicitly no hooks
Advanced Usage
Dynamic Path
Use context formatting for paths:
[tool.hatch.build.hooks.version]
path = "src/{name}/_version.py" # Uses project name
Version File in Multiple Locations
For monorepos or complex projects:
# hatch_build.py
from pathlib import Path
def initialize(version, build_config):
"""Write version to multiple locations."""
locations = [
"packages/core/version.py",
"packages/cli/version.py",
"packages/api/version.py",
]
template = f'__version__ = "{version}"'
for location in locations:
path = Path(location)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(template)
Version with Build Metadata
Include build information:
# hatch_build.py
import os
from datetime import datetime
from pathlib import Path
def initialize(version, build_config):
"""Create version file with build metadata."""
build_info = {
"version": version,
"build_date": datetime.now().isoformat(),
"build_host": os.environ.get("HOSTNAME", "unknown"),
"build_user": os.environ.get("USER", "unknown"),
"ci_build": os.environ.get("CI", "false"),
}
template = f"""
# Automatically generated build information
__version__ = "{build_info['version']}"
__build_date__ = "{build_info['build_date']}"
__build_host__ = "{build_info['build_host']}"
__build_user__ = "{build_info['build_user']}"
__ci_build__ = {build_info['ci_build']}
"""
version_file = Path("src/my_package/_build_info.py")
version_file.write_text(template)
Development Workflow
Editable Installs
Version files are NOT created during editable installs:
# Editable install
$ pip install -e .
# No _version.py created
# Regular install
$ pip install .
# _version.py is created
Handle this in your code:
try:
from ._version import __version__
except ImportError:
# In development mode
__version__ = "0.0.0+dev"
Git Ignore
Add generated version files to .gitignore:
# Generated version files
src/my_package/_version.py
*/_version.py
*/_build_info.py
Pre-commit Hook
Ensure version files aren't committed:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: check-version-file
name: Check version file not committed
entry: sh -c 'test ! -f src/my_package/_version.py'
language: system
files: _version\.py$
pass_filenames: false
Troubleshooting
File Not Created
Issue: Version file missing after build
Check:
- Hook is enabled in configuration
- Path is correct and parent directories exist
- No errors during build process
# Debug build
$ hatch build -v
# Look for version hook execution
Template Syntax Error
Issue: Invalid template format
# Wrong - missing quotes
template = __version__ = {version}
# Correct
template = '__version__ = "{version}"'
Pattern Not Matching
Issue: Existing file not updated
Debug pattern:
import re
pattern = r"__version__ = ['\"](?P<version>[^'\"]+)['\"]"
content = open("src/my_package/__init__.py").read()
match = re.search(pattern, content)
print(f"Match: {match.group('version') if match else 'No match'}")
Version Import Error
Issue: Can't import version at runtime
Solutions:
- Ensure version file is included in package
- Check import path is correct
- Add try/except for development mode
Best Practices
1. Use Dedicated Version Files
Keep version separate from other code:
# Good
path = "src/my_package/_version.py"
# Less ideal
path = "src/my_package/__init__.py" # Mixed with other code
2. Include Fallbacks
Always handle missing version files:
try:
from ._version import __version__
except ImportError:
__version__ = "unknown"
3. Document Generated Files
Add header to templates:
template = '''
# This file is automatically generated by the build process.
# Do not edit manually.
__version__ = "{version}"
'''
4. Test Version Access
Add tests for version availability:
def test_version():
"""Test that version is accessible."""
import my_package
assert hasattr(my_package, "__version__")
assert my_package.__version__ != "unknown"