Injecting Collections: Lists, Sets & Dictionaries¶
In the guides so far, we've mostly assumed a one-to-one relationship: you ask for Database, you get one Database.
But what about a one-to-many relationship? This is a very common scenario: * You have one Sender interface, but multiple implementations: EmailSender, SmsSender, and PushNotificationSender. * A NotificationService needs to get all of them. * A CommandBus needs a map of all CommandHandlers.
pico-ioc handles all of these scenarios automatically by recognizing collection types in your __init__ constructor.
1. Automatic List/Collection Injection¶
If you ask for a list or set of an interface (like List[Sender], Set[Sender], or Iterable[Sender]), pico-ioc will: 1. Find all components that implement or inherit from that interface (Sender). 2. Create an instance of each one. 3. Inject them as a list (regardless of whether you asked for a List, Set, or Iterable).
Step 1: Define Components¶
Define multiple components that implement the same interface (IService in this case).
# app/services.py
from typing import Protocol
from pico_ioc import component
class IService(Protocol):
def serve(self) -> str: ...
@component
class ServiceA(IService):
def serve(self) -> str:
return "A"
@component
class ServiceB(IService):
def serve(self) -> str:
return "B"
Step 2: Inject the Collection¶
Your consumer component simply requests List[IService], Set[IService], or Iterable[IService].
# app/consumer.py
from typing import List, Set, Iterable
from pico_ioc import component
from app.services import IService
@component
class Consumer:
def __init__(
self,
services_list: List[IService],
services_set: Set[IService]
):
# This will be [ServiceA(), ServiceB()]
self.services = services_list
# This will ALSO be [ServiceA(), ServiceB()]
self.services_set_as_list = services_set
print(f"Loaded {len(self.services)} services.")
# --- main.py ---
from pico_ioc import init
from app.consumer import Consumer
container = init(modules=["app.services", "app.consumer"])
consumer = container.get(Consumer) # Output: Loaded 2 services.
2. Automatic Dictionary Injection¶
This is a powerful feature for patterns like CQRS or strategy maps. pico-ioc can build a dictionary of components, using either strings or types as the dictionary keys.
Dict[str, T] (Keyed by Name)¶
If you request Dict[str, IService], pico-ioc will inject a dictionary where:
- Keys are the component's registered
name(from@component(name=...)). - Values are the component instances.
# app/services.py
from pico_ioc import component
@component(name="serviceA") # <-- Registered name
class ServiceA(IService):
...
@component(name="serviceB") # <-- Registered name
class ServiceB(IService):
...
# app/consumer.py
from typing import Dict
from app.services import IService
@component
class DictConsumer:
def __init__(self, service_map: Dict[str, IService]):
# service_map will be:
# {
# "serviceA": ServiceA(),
# "serviceB": ServiceB()
# }
self.service_map = service_map
def call_a(self):
return self.service_map["serviceA"].serve()
Dict[Type, T] (Keyed by Type)¶
If you request Dict[Type, IService], pico-ioc will inject a dictionary where:
- Keys are the class types of the components (e.g.,
ServiceA,ServiceB). - Values are the component instances.
This is extremely useful for building dispatch maps in patterns like CQRS.
# app/consumer.py
from typing import Dict, Type
from app.services import IService, ServiceA, ServiceB
@component
class TypeDictConsumer:
def __init__(self, service_map: Dict[Type, IService]):
# service_map will be:
# {
# ServiceA: ServiceA(),
# ServiceB: ServiceB()
# }
self.service_map = service_map
def call_a(self):
return self.service_map[ServiceA].serve()
3. Using Qualifiers to Filter Collections¶
What if you don't want all services, just a specific subset? This is where Qualifiers are used. Qualifiers act as filters for list and dictionary injections.
-
Define a Qualifier:
-
Tag Your Components:
-
Request a Filtered Collection: You use
typing.Annotatedto combine the collection type with theQualifiertag.from typing import List, Dict, Annotated @component class FilteredConsumer: def __init__( self, # Gets [ServiceA(), ServiceB()] fast_list: Annotated[List[IService], FAST_SERVICES], # Gets [ServiceB(), ServiceC()] slow_list: Annotated[List[IService], SLOW_SERVICES], # Gets {"serviceA": ServiceA(), "serviceB": ServiceB()} fast_map: Annotated[Dict[str, IService], FAST_SERVICES] ): ...
Summary: Injection Rules¶
List[T]: Injects alistof all components implementingT.Dict[str, T]: Injects adictmapping componentnameto the instance.Dict[Type, T]: Injects adictmapping componenttypeto the instance.Annotated[List[T], Q("tag")]: Injects alistof allTcomponents filtered by the "tag".Annotated[Dict[...], Q("tag")]: Injects adictof allTcomponents filtered by the "tag".
Next Steps¶
You now know how to register components, configure them, control their lifecycle, and inject specific lists or dictionaries. The final piece of the core user guide is learning how to test your application.
- Testing Applications: Learn how to use
overridesandprofilesto mock dependencies and test your services in isolation. See Testing Applications.