Tiny zero-dependency dependency injection container for Python services, CLIs, workers, and clean architecture applications.
Injex is useful when hand-wiring dependencies starts to get noisy, but a large framework-style container would be too much. It keeps the API small and uses normal Python type hints for constructor injection.
pip install injex- Zero dependencies: pure Python, easy to vendor, audit, and run anywhere.
- Typed constructor injection: dependencies are resolved from annotations.
- Production lifetimes: singleton, transient, and scoped services.
- Factories and instances: use custom creation logic or prebuilt objects.
- Named registrations: register multiple implementations of the same type.
- Optional dependencies:
Optional[T]works without special configuration. - Test overrides: swap real services for fakes in a small, explicit scope.
- Container validation: catch missing annotations, missing registrations, and dependency cycles before your app starts.
from injex import Container
class UserRepository:
def save(self, email: str) -> int:
return 42
class EmailSender:
def send_welcome(self, email: str) -> None:
print(f"Welcome, {email}")
class RegisterUser:
def __init__(self, repo: UserRepository, email_sender: EmailSender):
self.repo = repo
self.email_sender = email_sender
def execute(self, email: str) -> int:
user_id = self.repo.save(email)
self.email_sender.send_welcome(email)
return user_id
container = Container()
container.add_singleton(UserRepository)
container.add_singleton(EmailSender)
container.add_transient(RegisterUser)
container.assert_valid()
use_case = container.resolve(RegisterUser)
user_id = use_case.execute("ada@example.com")validate() checks the registered dependency graph without constructing your
services. That makes it safe for startup checks and CI smoke tests.
errors = container.validate()
if errors:
for error in errors:
print(error)
raise SystemExit(1)Use assert_valid() when you prefer a single exception with all validation
errors.
Use override() to replace a dependency only inside a with block.
class FakeEmailSender:
def __init__(self):
self.sent_to = []
def send_welcome(self, email: str) -> None:
self.sent_to.append(email)
fake_sender = FakeEmailSender()
with container.override(EmailSender, instance=fake_sender):
use_case = container.resolve(RegisterUser)
use_case.execute("test@example.com")
assert fake_sender.sent_to == ["test@example.com"]Scoped services are reused inside one scope and recreated for another scope.
from injex import Container
class RequestContext:
pass
container = Container()
container.add_scoped(RequestContext)
scope_a = container.create_scope()
scope_b = container.create_scope()
assert scope_a.resolve(RequestContext) is scope_a.resolve(RequestContext)
assert scope_a.resolve(RequestContext) is not scope_b.resolve(RequestContext)| Feature | Injex | dependency-injector | punq | lagom |
|---|---|---|---|---|
| Zero runtime dependencies | ✅ | ❌ | ✅ | ✅ |
| Type-hint constructor injection | ✅ | ✅ | ✅ | ✅ |
| Singleton / transient lifetimes | ✅ | ✅ | ✅ | ✅ |
| Scoped lifetime | ✅ | ✅ | ❌ | ✅ |
| Named registrations | ✅ | ✅ | ❌ | ✅ |
| Property injection | ✅ | ❌ | ❌ | ❌ |
| Temporary test overrides | ✅ | ✅ | ❌ | ✅ |
| Graph validation without object creation | ✅ | ❌ | ❌ | ❌ |
| Small API surface | ✅ | ❌ | ✅ | ✅ |
This table is not a benchmark. It shows the niche: Injex aims to be small and explicit while still covering common application wiring needs.
- Docs index
- Tutorial
- Validation guide
- API reference
- Clean architecture example
- CLI application example
- Testing overrides example
- Scoped lifetime example
- Factories example
- Named registrations example
| Method | Use when |
|---|---|
add_singleton(T, Impl) |
One instance should be reused for the app lifetime. |
add_transient(T, Impl) |
A new instance should be created on every resolve. |
add_scoped(T, Impl) |
One instance should be reused inside one scope. |
add_*_factory(T, factory) |
Construction needs custom code. |
add_instance(T, instance) |
You already have the object to use. |
resolve(T) |
Resolve one service from the root container. |
resolve_all(T) |
Resolve all unnamed implementations for a type. |
create_scope() |
Start a request, job, or message lifetime. |
override(T, ...) |
Temporarily replace a dependency in tests. |
validate() / assert_valid() |
Check wiring before startup. |
- Service-layer wiring in web APIs without coupling code to a web framework.
- Clean architecture use cases with repositories, gateways, and presenters.
- CLI tools where commands share configuration, clients, and services.
- Background workers and consumers with per-job or per-message scopes.
- Unit tests that need explicit dependency replacement.
Contributions are welcome when they keep the API small, tested, and practical. Useful changes usually improve documentation, typing, examples, or narrow edge cases without adding runtime dependencies.
See CONTRIBUTING.md for the local setup and contribution guidelines.