DuckDI is a minimal, type-safe, and architecture-friendly dependency injection library for Python.
It provides a clean interface to register and resolve dependencies at runtime using a TOML-based configuration, following the duck typing principle: "if it implements the expected methods, it’s good enough."
Ideal for developers who want clarity, zero magic, and full control over dependency resolution.
- ✅ Clean and lightweight API
- ✅ Zero runtime dependencies
- ✅ Fully type-safe (no introspection magic)
- ✅ Supports singleton and transient resolution
- ✅ Uses TOML to bind interfaces to adapters
- ✅ Works with
ABCand regular classes (no need for Protocols) - ✅ Clear and informative error messages
- ✅ Environment-based configuration (
INJECTIONS_PATH)
With Poetry:
poetry add duckdiOr using pip:
pip install duckdifrom duckdi import Interface
from abc import ABC, abstractmethod
@Interface
class IUserRepository(ABC):
@abstractmethod
def get_user(self, user_id: str) -> dict: ...from duckdi import register
class PostgresUserRepository(IUserRepository):
def get_user(self, user_id: str) -> dict:
return {"id": user_id, "name": "John Doe"}
register(PostgresUserRepository)You can also register it as a singleton:
register(PostgresUserRepository, is_singleton=True)Create a file called injections.toml:
[injections]
"i_user_repository" = "postgres_user_repository"Set the injection file path using the INJECTIONS_PATH environment variable:
export INJECTIONS_PATH=./injections.tomlfrom duckdi import Get
repo = Get(IUserRepository)
user = repo.get_user("123")
print(user) # {'id': '123', 'name': 'John Doe'}Raised when no injection payload file is found at the specified path.
Raised when the adapter registered does not implement the expected interface.
Raised when trying to register the same interface twice.
Raised when the same adapter is registered more than once.
duckdi/
├── pyproject.toml
├── README.md
├── src/
│ └── duckdi/
│ ├── __init__.py
│ ├── cli.py
│ ├── duck.py
│ ├── errors/
│ │ ├── __init__.py
│ │ ├── invalid_adapter_implementation_error.py
│ │ ├── interface_already_registered_error.py
│ │ ├── adapter_already_registered_error.py
│ │ └── missing_injection_payload_error.py
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── buffer_readers.py
│ │ └── to_snake.py
│ └── injections/
│ ├── injections_container.py
│ └── injections_payload.py
└── tests/
├── test_interface.py
├── test_register.py
└── test_get.py
You can register multiple adapters and resolve them dynamically based on the TOML mapping:
from duckdi import Interface, register, Get
@Interface
class INotifier:
def send(self, msg: str): ...
class EmailNotifier(INotifier):
def send(self, msg: str):
print(f"Sending email: {msg}")
register(EmailNotifier)
# injections.toml
# [injections]
# "i_notifier" = "email_notifier"
notifier = Get(INotifier)
notifier.send("Hello from DuckDI!")To run tests:
pytestOr via Makefile:
make testTo check static typing:
make checkLicensed under the MIT License.
See the LICENSE file for more information.
Made with ❤️ by PhePato
Pull requests, issues and ideas are always welcome!
