How to use service definition¶
This guide shows how to use the ServiceDefinition class for advanced service configuration.
When to use service definition¶
Use ServiceDefinition when you need:
Custom factory functions for service creation
Named service registrations (for future multi-registration support)
Pre-configured service definitions to share across modules
More explicit control over service configuration
Basic usage¶
Creating a service definition¶
from hdmi import ServiceDefinition
# Basic definition (singleton is default)
definition = ServiceDefinition(MyService) # singleton (scoped=False, transient=False)
# Or explicitly specify scoped or transient
scoped_definition = ServiceDefinition(MyService, scoped=True)
transient_definition = ServiceDefinition(MyService, transient=True)
# Note: service_type must be positional, boolean flags must be keyword
# This will fail:
# definition = ServiceDefinition(service_type=MyService) # TypeError
# definition = ServiceDefinition(MyService, True) # TypeError - use scoped=True
Registering with container builder¶
from hdmi import ContainerBuilder, ServiceDefinition
builder = ContainerBuilder()
# Register a ServiceDefinition
definition = ServiceDefinition(DatabaseService) # singleton (default)
builder.register(definition)
# IMPORTANT: Cannot specify boolean flags when registering ServiceDefinition
# This will raise ValueError:
# builder.register(definition, scoped=True) # ValueError!
# For simple registrations, use the shorthand:
builder.register(UserService, scoped=True) # scoped service
Using custom factories¶
ServiceDefinition allows you to provide a custom factory function:
from hdmi import ContainerBuilder, ServiceDefinition
def create_database_connection():
"""Custom factory that configures the database."""
return DatabaseConnection(
host="localhost",
port=5432,
database="myapp"
)
# Create definition with factory (singleton by default)
db_definition = ServiceDefinition(
DatabaseConnection,
factory=create_database_connection
)
import asyncio
async def main():
builder = ContainerBuilder()
builder.register(DatabaseConnection, factory=create_database_connection)
async with builder.build() as container:
db = await container.get(DatabaseConnection) # Uses the factory
asyncio.run(main())
Factory requirements¶
Must be callable (function, method, or callable object)
Must be passed as keyword argument
Will be validated when ServiceDefinition is created
# Valid factories
def factory_function():
return MyService()
definition = ServiceDefinition(MyService, factory=factory_function)
# Invalid - not callable
definition = ServiceDefinition(MyService, factory="not_callable")
# Raises: ValueError: factory must be callable
Using lifecycle hooks¶
Services can have initializers (called after instantiation) and finalizers (called when the container exits). Both can be sync or async.
Initializers¶
Initializers are called after a service is instantiated:
import asyncio
from hdmi import ContainerBuilder
class DatabaseConnection:
def __init__(self):
self.connected = False
def connect(self):
self.connected = True
async def main():
builder = ContainerBuilder()
# Sync initializer
builder.register(
DatabaseConnection,
initializer=lambda db: db.connect()
)
async with builder.build() as container:
db = await container.get(DatabaseConnection)
assert db.connected # initializer was called
asyncio.run(main())
Async initializers are also supported:
async def async_connect(db):
await db.connect_async()
builder.register(DatabaseConnection, initializer=async_connect)
Finalizers¶
Finalizers are called when the container or scope exits:
import asyncio
from hdmi import ContainerBuilder
class DatabaseConnection:
def __init__(self):
self.connected = True
def disconnect(self):
self.connected = False
async def main():
builder = ContainerBuilder()
builder.register(
DatabaseConnection,
finalizer=lambda db: db.disconnect()
)
db_instance = None
async with builder.build() as container:
db_instance = await container.get(DatabaseConnection)
assert db_instance.connected
# After exiting the context, finalizer is called
assert not db_instance.connected
asyncio.run(main())
Combined initializer and finalizer¶
builder.register(
DatabaseConnection,
initializer=lambda db: db.connect(),
finalizer=lambda db: db.disconnect()
)
Controlling autowiring¶
The autowire parameter controls whether a service is automatically injected into
optional dependencies. By default, autowire=True.
from hdmi import ContainerBuilder
class OptionalFeature:
pass
class MyService:
def __init__(self, feature: OptionalFeature | None = None):
self.feature = feature
# With autowire=True (default), OptionalFeature is injected if registered
builder = ContainerBuilder()
builder.register(OptionalFeature) # autowire=True by default
builder.register(MyService)
async with builder.build() as container:
service = await container.get(MyService)
assert service.feature is not None # OptionalFeature was injected
# With autowire=False, the service is not injected into optional dependencies
builder = ContainerBuilder()
builder.register(OptionalFeature, autowire=False)
builder.register(MyService)
async with builder.build() as container:
service = await container.get(MyService)
assert service.feature is None # Default value used instead
Named services¶
ServiceDefinition supports named registrations for future multi-registration scenarios:
primary_db = ServiceDefinition(
DatabaseConnection,
name="primary" # singleton by default
)
readonly_db = ServiceDefinition(
DatabaseConnection,
name="readonly",
factory=create_readonly_connection # singleton by default
)
builder = ContainerBuilder()
builder.register(primary_db)
builder.register(readonly_db)
Note
Named service resolution is planned for future releases. Currently, names are stored but not used for resolution.
Exporting and importing definitions¶
ServiceDefinition is now exported from the main hdmi package:
# Direct import from hdmi
from hdmi import ServiceDefinition, ContainerBuilder
# Create reusable definitions (all singleton by default)
STANDARD_SERVICES = [
ServiceDefinition(LoggingService),
ServiceDefinition(ConfigService),
ServiceDefinition(CacheService),
]
# Use in multiple places
def configure_container():
builder = ContainerBuilder()
for definition in STANDARD_SERVICES:
builder.register(definition)
return builder
Common patterns¶
Module-level definitions¶
Define services at module level for reuse:
# services.py
from hdmi import ServiceDefinition
from myapp.database import DatabaseConnection
from myapp.cache import RedisCache
# Export pre-configured definitions (singleton by default)
database_definition = ServiceDefinition(
DatabaseConnection,
factory=lambda: DatabaseConnection.from_env()
)
cache_definition = ServiceDefinition(
RedisCache,
name="main_cache" # singleton by default
)
# main.py
from hdmi import ContainerBuilder
from services import database_definition, cache_definition
builder = ContainerBuilder()
builder.register(database_definition)
builder.register(cache_definition)
Testing with factories¶
Use factory functions to override services in tests:
# test_services.py
import pytest
from hdmi import ContainerBuilder
@pytest.mark.anyio
async def test_with_mock_database():
# Create mock with custom factory (singleton by default)
builder = ContainerBuilder()
builder.register(DatabaseConnection, factory=lambda: MockDatabase())
builder.register(UserService) # depends on DatabaseConnection
async with builder.build() as container:
service = await container.get(UserService)
# UserService will use the mock database
Best practices¶
Use shorthand for simple registrations: If you only need type and scope, use
builder.register(Type, scoped=True)orbuilder.register(Type, transient=True)directly.Always use async context manager: Use
async with builder.build() as container:to ensure proper lifecycle management and finalizer execution.Use initializers for setup: Rather than calling setup methods manually, use initializers to ensure services are properly configured.
Use finalizers for cleanup: Register finalizers for services that need cleanup (database connections, file handles, etc.) to ensure resources are released.
Use autowire=False for optional features: When a service should only be injected explicitly, not automatically into optional dependencies.
Validate early: ServiceDefinition validates the factory, initializer, and finalizer parameters immediately, catching errors early.
See also¶
API reference for complete ServiceDefinition API
Architecture deep dive for understanding service lifecycles
Tutorials for step-by-step examples