ditto is a simple and lightweight dependency injection tool for Python, with support for nullable arguments and parent class registration.
- Easy-to-use decorators for dependency injection
- Supports class and instance-based services
- Sync and Async support
- Nullable argument handling
- Parent class registration
pip install pyditto
Declare your dependencies to ditto on application start:
import ditto as di
class Pokemon:
def attack(self) -> str:
pass
class FirePokemon(Pokemon):
def attack(self) -> str:
return 'Ember ! 🔥'
di.register(FirePokemon) # register as a class
di.register(FirePokemon()) # or as an object
Then you can inject your dependency into your class:
@di.inject
class Team:
charmander: FirePokemon
def battle(self):
self.charmander.attack()
team = Team()
team.battle() # 'Ember ! 🔥'
Or into an isolated function within your class:
class Team:
@di.inject
def battle(self, charmander: FirePokemon):
charmander.attack()
team = Team()
team.battle() # 'Ember ! 🔥'
Or just within a bare function:
@di.inject
def battle(charmander: FirePokemon):
charmander.attack()
battle() # 'Ember ! 🔥'
ditto uses the lowercase name of the class as the key in its registry:
di.register(Charmander)
# These will all retrieve the Charmander instance:
fire_pokemon = di.ServiceRegistry.get_instance().get('firepokemon')
charmander = di.ServiceRegistry.get_instance().get('charmander')
This is particularly useful to understand when working with optional dependencies or when overriding parent class implementations.
ditto supports dependency injection for asynchronous functions:
import asyncio
class AsyncPokemon:
async def attack(self) -> str:
await asyncio.sleep(1)
return "Async attack!"
di.register(AsyncPokemon)
@di.inject
async def async_battle(pokemon: AsyncPokemon):
result = await pokemon.attack()
print(result)
asyncio.run(async_battle()) # Output: Async attack!
ditto supports nullable arguments, allowing you to specify optional dependencies:
from typing import Optional
@di.inject
def train_pokemon(charmander: FirePokemon, optional_pokemon: Optional[Pokemon] = None):
charmander.attack()
if optional_pokemon:
optional_pokemon.attack()
else:
print("No optional Pokemon available")
# This will work even if Pokemon is not registered
train_pokemon()
# You can also explicitly pass None for optional arguments
train_pokemon(optional_pokemon=None)
In this example:
charmander
is a required dependency and must be registered.optional_pokemon
is an optional dependency. If it's not registered,None
will be injected.
Note on Type Registration for Optional Parameters:
For both Pokemon
and Optional[Pokemon]
, ditto uses the same key ('pokemon'
) in its registry. The Optional
wrapper doesn't affect the registration key; it only changes how ditto handles the case when the service isn't found.
When you register a service class, ditto automatically registers it under its immediate parent class as well:
class Pokemon:
pass
class FirePokemon(Pokemon):
pass
class Charmander(FirePokemon):
pass
di.register(Charmander)
@di.inject
def train(fire_pokemon: FirePokemon, charmander: Charmander):
print(f"Training: {fire_pokemon.__class__.__name__}, {charmander.__class__.__name__}")
train() # Output: Training: Charmander, Charmander
@di.inject
def catch(pokemon: Pokemon):
print(f"Trying to catch: {pokemon.__class__.__name__}")
catch() # This will raise a ValueError: Service 'pokemon' not found.
In this example:
- Registering
Charmander
also makes it available whenFirePokemon
(its immediate parent class) is requested. - However,
Pokemon
(the grandparent class) is not automatically registered. - This allows for one level of inheritance in dependency injection.
-
Missing dependencies: If a required dependency is not registered, ditto will raise a
ValueError
:@di.inject def use_unregistered(service: NotInPokedexPokemon): pass use_unregistered() # Raises ValueError: Service 'notinpokedexpokemon' not found.
-
Circular dependencies: ditto doesn't automatically resolve circular dependencies. Be cautious when designing your dependency graph.
When multiple classes are registered for the same type, ditto will use the most recently registered one:
class Pikachu(Pokemon):
pass
class Charmander(Pokemon):
pass
di.register(Pikachu)
di.register(Charmander)
@di.inject
def catch(pokemon: Pokemon):
print(f"Caught a {pokemon.__class__.__name__}")
catch() # Output: Caught a Charmander
- Register dependencies at the application's entry point.
- Use interfaces (abstract base classes) for better decoupling.
- Avoid registering too many concrete implementations to keep your dependency graph simple.
To unregister a service:
# Unregister a service
ServiceRegistry.get_instance()._services.pop('pokemon', None)
- ditto doesn't support automatic constructor injection. Parameters must be explicitly annotated.
- Circular dependencies are not automatically resolved and may cause issues if not carefully managed.
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.