Hook Dependencies
Build hooks can declare dependencies that must be installed before the build process. Help users understand how to properly manage hook-specific dependencies, which are separate from the project's runtime dependencies, by referencing this documentation.
Overview
Hook dependencies allow:
- Third-party build hooks to declare what they need
- Custom hooks to specify external tools or libraries
- Lazy import of dependencies (only when hooks are used)
- Proper build environment setup
Types of Hook Dependencies
1. Third-Party Hook Dependencies
Install external build hooks via build-system.requires:
[build-system]
requires = [
"hatchling",
"hatch-cython>=0.11",
"hatch-mypyc>=1.0",
]
Then configure them:
[tool.hatch.build.hooks.cython]
# Hook is now available
[tool.hatch.build.hooks.mypyc]
# Hook is now available
2. Hook-Level Dependencies
Additional dependencies specific to a hook:
[tool.hatch.build.hooks.custom]
dependencies = ["jinja2>=2.11", "pillow>=8.0"]
These are installed when the hook runs.
3. Runtime Dependencies
Make hooks depend on the project's runtime dependencies:
[tool.hatch.build.hooks.custom]
require-runtime-dependencies = true
This installs all packages listed in [project.dependencies].
4. Optional Feature Dependencies
Depend on specific optional features:
[tool.hatch.build.hooks.custom]
require-runtime-features = ["imaging", "pdf"]
This installs packages from [project.optional-dependencies] for those features.
Configuration
Hook-Specific Dependencies
[tool.hatch.build.hooks.custom]
path = "hatch_build.py"
dependencies = [
"jinja2>=2.11",
"pillow>=8.0",
"click>=7.0,<9.0",
]
Dependencies are specified as a list of version specifiers (same format as [project.dependencies]).
Global vs Target-Specific
Dependencies can be specified globally or per-target:
# Global hook dependencies
[tool.hatch.build.hooks.custom]
dependencies = ["jinja2>=2.11"]
# Target-specific dependencies
[tool.hatch.build.targets.wheel.hooks.custom]
dependencies = ["pillow>=8.0"]
# Both will be installed when building wheels
# Only jinja2 when building sdist
Runtime Dependency Requirements
Make hooks depend on project runtime dependencies:
[tool.hatch.build.hooks.custom]
require-runtime-dependencies = true
Or depend on specific optional features:
[tool.hatch.build.hooks.custom]
require-runtime-features = ["docs", "testing"]
Implementation
Declaring Dependencies in Custom Hooks
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface):
def dependencies(self) -> list[str]:
"""Declare dependencies required by this hook"""
return [
"jinja2>=2.11",
"pillow>=8.0",
"PyYAML>=5.0",
]
def initialize(self, version: str, build_data: dict) -> None:
# These imports are safe now - dependencies are installed
from jinja2 import Template, Environment, FileSystemLoader
from PIL import Image
import yaml
# Use the libraries
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('build.jinja2')
Lazy Imports
Dependencies declared via dependencies() should be imported inside methods, not at module level:
# GOOD: Import inside method
class CustomBuildHook(BuildHookInterface):
def dependencies(self) -> list[str]:
return ["jinja2>=2.11"]
def initialize(self, version: str, build_data: dict) -> None:
from jinja2 import Template # Safe here
template = Template(...)
# BAD: Import at module level
from jinja2 import Template # May fail if hook is disabled
class CustomBuildHook(BuildHookInterface):
def dependencies(self) -> list[str]:
return ["jinja2>=2.11"]
def initialize(self, version: str, build_data: dict) -> None:
template = Template(...) # Uses module-level import
Dynamic Dependencies
Return different dependencies based on conditions:
class CustomBuildHook(BuildHookInterface):
def dependencies(self) -> list[str]:
"""Dependencies depend on configuration"""
deps = []
if self.config.get('enable-cython'):
deps.append("cython>=0.29")
if self.config.get('enable-numpy'):
deps.append("numpy>=1.19")
return deps
Configuration:
[tool.hatch.build.hooks.custom]
path = "hatch_build.py"
enable-cython = true
enable-numpy = true
Common Dependency Patterns
Tool-Based Hook
class CustomBuildHook(BuildHookInterface):
def dependencies(self) -> list[str]:
return ["sass>=1.50"] # Sass compiler
def initialize(self, version: str, build_data: dict) -> None:
import subprocess
subprocess.run(["sass", "styles/main.scss:dist/main.css"], check=True)
Configuration:
[tool.hatch.build.hooks.custom]
dependencies = ["sass>=1.50"]
Multi-Library Hook
class CustomBuildHook(BuildHookInterface):
def dependencies(self) -> list[str]:
return [
"jinja2>=2.11",
"pillow>=8.0",
"click>=7.0,<9.0",
"PyYAML>=5.0",
]
def initialize(self, version: str, build_data: dict) -> None:
from jinja2 import Environment, FileSystemLoader
from PIL import Image
import click
import yaml
# Use all libraries
self.generate_images()
self.build_config()
Platform-Specific Dependencies
import platform
class CustomBuildHook(BuildHookInterface):
def dependencies(self) -> list[str]:
deps = ["jinja2>=2.11"]
if platform.system() == "Windows":
deps.append("pywin32>=300")
return deps
Conditional Optional Dependencies
class CustomBuildHook(BuildHookInterface):
def dependencies(self) -> list[str]:
"""Include optional dependencies if configured"""
deps = ["jinja2>=2.11"]
# Configuration-driven dependencies
if self.config.get('enable-pdf'):
deps.append("reportlab>=3.5")
if self.config.get('enable-compression'):
deps.append("lz4>=3.1")
return deps
def initialize(self, version: str, build_data: dict) -> None:
from jinja2 import Template
if self.config.get('enable-pdf'):
from reportlab.pdfgen import canvas
Version Specifiers
Hook dependencies use the same version specifier syntax as project dependencies:
| Specifier | Example | Meaning |
|---|---|---|
| No specifier | package |
Any version |
| Exact | package==1.0.0 |
Exactly 1.0.0 |
| Compatible | package~=1.4.2 |
>=1.4.2, <1.5 |
| Minimum | package>=1.0 |
1.0 or later |
| Maximum | package<2.0 |
Before 2.0 |
| Range | package>=1.0,<2.0 |
1.0 or later, before 2.0 |
| Not equal | package!=1.5 |
Any except 1.5 |
| Complex | package>=1.0,!=1.5,<2.0 |
Multiple constraints |
Configuration Examples
Simple Custom Hook with Dependencies
[tool.hatch.build.hooks.custom]
path = "hatch_build.py"
dependencies = ["jinja2"]
Multiple Hooks with Dependencies
[tool.hatch.build.hooks.generate]
path = "hooks/generate.py"
dependencies = ["jinja2>=2.11"]
[tool.hatch.build.hooks.compile]
path = "hooks/compile.py"
dependencies = ["cython>=0.29"]
Target-Specific Dependencies
[tool.hatch.build.targets.wheel.hooks.custom]
dependencies = ["cython>=0.29"]
[tool.hatch.build.targets.sdist.hooks.custom]
dependencies = []
Runtime Dependencies
[tool.hatch.build.hooks.custom]
require-runtime-dependencies = true
This installs all packages from [project.dependencies].
Feature Dependencies
[tool.hatch.build.hooks.custom]
require-runtime-features = ["docs", "testing"]
This installs packages from:
[project.optional-dependencies]→docs[project.optional-dependencies]→testing
Important Restrictions
Hook Dependency Rule
The hook dependency itself must be defined in build-system.requires:
[build-system]
requires = [
"hatchling",
"my-custom-hook-plugin", # Hook itself must be here
]
[tool.hatch.build.hooks.custom]
dependencies = ["jinja2"] # Extra dependencies
Why: The hook must be importable to call its dependencies() method. If the hook itself were in the dependencies() list, it would create a circular dependency.
Third-Party Hook Dependencies
[build-system]
requires = [
"hatchling",
"hatch-cython>=0.11", # Third-party hook
]
[tool.hatch.build.hooks.cython]
dependencies = ["cython>=0.29"] # Extra dependencies
Lazy Import Rule
Always lazy-import (inside methods) any packages declared as dependencies:
# Correct: Import inside method
def initialize(self, version: str, build_data: dict) -> None:
from jinja2 import Template # Imported here
# Incorrect: Import at module level
from jinja2 import Template # May fail if dependencies not installed
class CustomBuildHook(BuildHookInterface):
def initialize(self, version: str, build_data: dict) -> None:
template = Template(...) # Uses module-level import
Troubleshooting
Module Not Found Error
If you get ModuleNotFoundError for a dependency:
- Check it's declared in
dependencies()method - Verify the package name matches the import name (sometimes different)
- Check the version specifier is correct
- Ensure the import is lazy (inside a method, not module-level)
Dependency Conflict
If dependencies conflict with project dependencies:
- Use compatible versions:
dependencies = ["jinja2>=2.11,<4.0"] - Consider splitting hooks by target: separate wheel and sdist hooks
- Use optional features instead of declaring separate dependencies
Conditional Dependencies Not Working
Ensure you're using configuration, not environment:
# Correct: Use self.config
def dependencies(self) -> list[str]:
if self.config.get('enable-cython'):
return ["cython"]
return []
# Incorrect: Using environment variables
def dependencies(self) -> list[str]:
if os.getenv('ENABLE_CYTHON'):
return ["cython"] # Environment not available yet
return []
Best Practices
- Declare all dependencies: Every import should have a corresponding dependency declaration
- Use version constraints: Be specific about version requirements
- Lazy imports: Import dependencies only when needed (inside methods)
- Document dependencies: Comment why each dependency is needed
- Minimize dependencies: Use only what's necessary
- Consider optional features: Use runtime feature dependencies for optional compilation
Related Topics
- Configuration Basics - Hook configuration
- BuildHookInterface Reference -
dependencies()method - Custom Build Hooks - Writing hooks with dependencies