Skip to content

Migration Guide: Upgrading to modern-di 2.x

This document describes the changes required to migrate from modern-di 1.x versions to modern-di 2.x.

Overview

The migration to modern-di 2.x involves several key changes in the API, including: - Simplified container architecture with a single Container class - Updated provider API with keyword-only arguments - Removal of several provider types (Singleton, Resource, Dict, List) - Changes in caching mechanism - Updated integration packages

Key Changes

1. Container Changes

The AsyncContainer and SyncContainer classes have been merged into a single Container class. The new container supports both synchronous and asynchronous operations.

Before (1.x):

from modern_di import AsyncContainer, SyncContainer

# Asynchronous container
async_container = AsyncContainer(groups=ALL_GROUPS)
async_container.enter()

# Synchronous container
sync_container = SyncContainer(groups=ALL_GROUPS)
sync_container.enter()

After (2.x):

from modern_di import Container

# Single container for both sync and async operations
container = Container(groups=ALL_GROUPS)
# No need to explicitly enter the container

# For async cleanup
await container.close_async()

# For sync cleanup
container.close_sync()

2. Provider API Changes

All providers now use keyword-only arguments for better clarity and consistency.

Before (1.x):

from modern_di import Scope, providers


factory = providers.Factory(Scope.REQUEST, MyClass, arg1="value1", arg2="value2")

After (2.x):

from modern_di import Scope, providers


factory = providers.Factory(scope=Scope.REQUEST, creator=MyClass, kwargs={"arg1": "value1", "arg2": "value2"})

3. Removed Provider Types

Several provider types have been removed and their functionality consolidated into the Factory provider:

Singleton Provider

Before (1.x):

singleton = providers.Singleton(Scope.APP, create_singleton)

After (2.x):

# Use Factory with cache settings
singleton = providers.Factory(
    scope=Scope.APP, 
    creator=create_singleton,
    cache_settings=providers.CacheSettings()
)

Resource Provider

Before (1.x):

resource = providers.Resource(Scope.REQUEST, create_resource)

After (2.x):

# Resources can be replaced with Factory with cache_settings with finalizer defined
resource = providers.Factory(
    scope=Scope.REQUEST,
    creator=create_resource,
    cache_settings=providers.CacheSettings(
        finalizer=lambda resource: resource.close(),
        clear_cache=False
    )
)

Dict and List Providers

These providers have been removed entirely. Use Factory providers with appropriate creator functions instead.

Before (1.x):

my_dict = providers.Dict(Scope.REQUEST, key1=provider1, key2=provider2)
my_list = providers.List(Scope.REQUEST, provider1, provider2, provider3)

After (2.x):

from dataclasses import dataclass
from typing import List

@dataclass(kw_only=True, slots=True, frozen=True)
class UserService:
    name: str
    age: int

@dataclass(kw_only=True, slots=True, frozen=True)
class AuthService:
    token: str
    expiry: int

# Define providers for UserService and AuthService first
user_service_provider = providers.Factory(creator=UserService)
auth_service_provider = providers.Factory(creator=AuthService)

# For dictionaries
def create_services_dict(user_service: UserService, auth_service: AuthService) -> dict[str, object]:
    return {
        "user": user_service,
        "auth": auth_service
    }

my_dict = providers.Factory(creator=create_services_dict)

# For lists
def create_service_list(user_service: UserService, auth_service: AuthService) -> List[object]:
    return [user_service, auth_service]

my_list = providers.Factory(creator=create_service_list)

4. Caching Changes

Caching is now handled through CacheSettings in Factory providers.

Before (1.x):

# Singleton was automatically cached
singleton = providers.Singleton(Scope.APP, create_singleton)

After (2.x):

# Explicit cache settings
singleton = providers.Factory(
    creator=create_singleton,
    cache_settings=providers.CacheSettings()
)

# Cache settings with finalizer
cached_with_cleanup = providers.Factory(
    creator=create_resource,
    cache_settings=providers.CacheSettings(
        finalizer=lambda resource: resource.close(),
        clear_cache=False
    )
)

5. Container Building and Scoping

Child container creation continues to support context managers in 2.x — use with / async with for automatic cleanup. The explicit close_sync() / close_async() methods are also available for callers that need to manage the container lifecycle manually.

Before (1.x):

# Async container
async with container.build_child_container(context=context, scope=Scope.REQUEST) as request_container:
    # Use request_container

# Sync container
with container.build_child_container(context=context, scope=Scope.REQUEST) as request_container:
    # Use request_container

After (2.x):

# Same context-manager form continues to work
with container.build_child_container(context=context, scope=Scope.REQUEST) as request_container:
    # Use request_container

async with container.build_child_container(context=context, scope=Scope.REQUEST) as request_container:
    # Use request_container

# If you need manual lifecycle control, call close_sync() or await close_async() yourself
request_container = container.build_child_container(context=context, scope=Scope.REQUEST)
# Use request_container
request_container.close_sync()  # or: await request_container.close_async()

6. Provider Resolution

Resolution methods have been simplified.

Before (1.x):

# Async resolution
instance = await container.resolve_provider(provider)
instance = await container.resolve(SomeType)

# Sync resolution
instance = container.sync_resolve_provider(provider)
instance = container.sync_resolve(SomeType)

After (2.x):

# now resolving is sync only
instance = container.resolve_provider(provider)
instance = container.resolve(SomeType)

!!! note "Async finalizers are still supported" Only resolution became sync-only in 2.x. Async finalizers (cleanup functions) are still fully supported via CacheSettings(finalizer=async_cleanup_fn) and await container.close_async(). The distinction: you cannot await during dependency resolution, but you can use async functions to clean up resources when a container is closed.

Migration Steps

  1. Update Dependencies: Ensure all modern-di packages are updated to 2.x versions
  2. Update Container Initialization: Replace AsyncContainer/SyncContainer with Container
  3. Update Provider Definitions:
  4. Replace positional arguments with keyword arguments
  5. Replace Singleton and Resource with Factory using CacheSettings
  6. Remove Dict and List providers, replace with Factory creators
  7. Update Container Building: Continue to use with / async with for automatic cleanup; close_sync() / close_async() are also available for manual lifecycle control
  8. Update Provider Resolution: Remove sync_ prefixes and await keywords

Breaking Changes

  1. AsyncContainer and SyncContainer classes removed (use Container instead)
  2. Singleton, Resource, Dict, and List provider types removed
  3. All provider constructors now use keyword-only arguments
  4. Provider resolution methods simplified (no sync_ prefix)
  5. Integration packages updated with new APIs
  6. Provider casting mechanism changed (.cast attribute removed)