Skip to content

DuplicateProviderTypeError

Symptom

modern_di.exceptions.registration.DuplicateProviderTypeError: Provider is duplicated by type <class 'myapp.dup.DatabaseConfig'>.
  - Configs.primary: Factory (myapp.dup:12)
  - Configs.replica: Factory (myapp.dup:13)
Set bound_type=None on one of them to make it resolvable by reference only.
See: https://modern-di.modern-python.org/troubleshooting/duplicate-type-error/

It is raised when the second provider is registered: by Container(groups=[...]) at startup, or by container.add_providers(...). Each bullet names one provider, with the one registered first on top. A bullet shows the group attribute the provider was declared as, its kind, and the module and line of the call that built it, such as Factory(...). A provider built outside a group class body has no attribute, so its bullet starts with the kind. .first_provider and .second_provider hold the provider objects.

The class descends from RegistrationError, then ModernDIError, then RuntimeError, so except DuplicateProviderTypeError, except RegistrationError and except RuntimeError all catch it. See Errors and exceptions.

Cause

modern-di resolves by type, so each type maps to at most one provider. Two providers end up with the same bound_type when:

  • two factories' creators return the same type, such as a primary and a replica DatabaseConfig;
  • two groups passed to the same container each declare a provider for the type.

Only distinct provider objects collide. When the same provider object arrives more than once, it is registered once and the repeats are ignored. That covers a group passed together with its subclass, one group passed twice, one provider assigned in two groups, and a provider passed again to container.add_providers(...).

Fix

If the two providers stand for different things, give each its own type. A thin subclass is enough, and each consumer asks for the one it needs:

class DatabaseConfig:
    def __init__(self, connection_string: str) -> None:
        self.connection_string = connection_string


class PrimaryConfig(DatabaseConfig): ...


class ReplicaConfig(DatabaseConfig): ...


class Configs(Group):
    primary = providers.Factory(
        PrimaryConfig, scope=Scope.APP, kwargs={"connection_string": "postgresql://primary"}
    )
    replica = providers.Factory(
        ReplicaConfig, scope=Scope.APP, kwargs={"connection_string": "postgresql://replica"}
    )


container = Container(groups=[Configs])
assert container.resolve(ReplicaConfig).connection_string == "postgresql://replica"

Otherwise set bound_type=None on one provider to take it out of the by-type lookup, and hand it to its consumers through kwargs. Every provider that would collide needs it, consumers included:

class Repository:
    def __init__(self, db_config: DatabaseConfig) -> None:
        self.db_config = db_config


class MyGroup(Group):
    primary_db_config = providers.Factory(
        DatabaseConfig,
        scope=Scope.APP,
        kwargs={"connection_string": "postgresql://primary"},
    )
    secondary_db_config = providers.Factory(
        DatabaseConfig,
        scope=Scope.APP,
        bound_type=None,
        kwargs={"connection_string": "postgresql://secondary"},
    )

    primary_repository = providers.Factory(Repository, scope=Scope.APP)
    secondary_repository = providers.Factory(
        Repository,
        scope=Scope.APP,
        bound_type=None,
        kwargs={"db_config": secondary_db_config},
    )


container = Container(groups=[MyGroup])
assert container.resolve(Repository).db_config.connection_string == "postgresql://primary"
secondary = container.resolve_provider(MyGroup.secondary_repository)
assert secondary.db_config.connection_string == "postgresql://secondary"

container.resolve(DatabaseConfig) and container.resolve(Repository) return the primary pair. The secondary pair is reachable only by reference, through container.resolve_provider(...) or as a kwargs value.

To make an abstract type resolve to a concrete implementation, use an Alias instead of a second factory.

See also