Tiny typed dependency injection for Python apps
Project description
Injex
Tiny typed dependency injection for Python apps that want explicit wiring without a framework-sized container.
Injex keeps constructor injection boring: normal type hints, zero runtime dependencies, scoped lifetimes, test overrides, and graph validation before your app starts. It is designed for services, CLIs, workers, and clean architecture code that should stay framework-agnostic.
pip install injex
Website: vshulcz.github.io/injex
Use Injex when
- you have a service layer reused by an API, CLI, worker, and tests;
- constructors already describe dependencies with type hints;
- test doubles should replace external services without changing production wiring;
- startup should catch missing registrations before the first request or job.
Skip Injex when
- a few manual constructor calls are still clear enough;
- your framework dependency system already covers every entrypoint;
- you need a large provider/configuration DSL.
Why Injex?
- Zero dependencies: pure Python, easy to vendor, audit, and run anywhere.
- Typed constructor injection: dependencies are resolved from annotations.
- Framework-agnostic: use the same wiring in web apps, workers, CLIs, and tests.
- 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.
Where it fits
Injex is useful when manual wiring starts to spread across your entrypoints, but
providers, global state, or a framework-specific container would be too much.
Common patterns:
- Service layer: wire repositories, gateways, clients, and use cases once at startup.
- CLIs: share configuration, API clients, and commands without module-level singletons.
- Workers: create one scope per job or message while reusing long-lived clients.
- Tests: override slow or external dependencies inside one
withblock. - Clean architecture: keep application code depending on interfaces instead of framework-specific dependency hooks.
Quick start
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 wiring before startup
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.
Testing with overrides
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"]
Scopes for request-style lifetimes
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 comparison
| 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.
Documentation and examples
- Docs site
- Docs index
- Tutorial
- Validation guide
- Why Injex
- Comparison guide
- Usage scenarios
- API reference
- Article: When Python manual wiring turns into copy-paste architecture
- Clean architecture example
- CLI application example
- FastAPI application service example
- Testing overrides example
- Scoped lifetime example
- Factories example
- Named registrations example
API at a glance
| 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. |
Common use cases
- 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.
Contributors
Thanks to the people improving Injex through issues, reviews, and pull requests:
- Muhammad Saqib Atif — FastAPI example.
- mahek —
resolve_all()documentation recipe. - oppnc — nested override regression tests.
- YuuGR1337 — README article link.
Contributing
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file injex-1.2.1.tar.gz.
File metadata
- Download URL: injex-1.2.1.tar.gz
- Upload date:
- Size: 18.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8619217d62640e29edf8d66e7dc65690479ee939e01004a08a566d6082704a0a
|
|
| MD5 |
49332dcd13f1625a3d452aca909d92d8
|
|
| BLAKE2b-256 |
6e665019c75e0a0fed8d5500297d3df1acba4a09eb805bda20e0aa6b2436492e
|
Provenance
The following attestation bundles were made for injex-1.2.1.tar.gz:
Publisher:
release.yml on vshulcz/injex
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
injex-1.2.1.tar.gz -
Subject digest:
8619217d62640e29edf8d66e7dc65690479ee939e01004a08a566d6082704a0a - Sigstore transparency entry: 1652300842
- Sigstore integration time:
-
Permalink:
vshulcz/injex@0210662a1eeed793a018ac49a0ccaebbaee250ba -
Branch / Tag:
refs/tags/v1.2.1 - Owner: https://github.com/vshulcz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0210662a1eeed793a018ac49a0ccaebbaee250ba -
Trigger Event:
release
-
Statement type:
File details
Details for the file injex-1.2.1-py3-none-any.whl.
File metadata
- Download URL: injex-1.2.1-py3-none-any.whl
- Upload date:
- Size: 10.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe024f56a16aa1168ea091c069c77352ca8f963f994545db42a6ce3bc0eb2dc3
|
|
| MD5 |
254f5447f2f2e89726b1da3d02d5087f
|
|
| BLAKE2b-256 |
795308279c3053ccdd372f28c018756f5b9e539fbc25f5ec381e3e97dac5a050
|
Provenance
The following attestation bundles were made for injex-1.2.1-py3-none-any.whl:
Publisher:
release.yml on vshulcz/injex
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
injex-1.2.1-py3-none-any.whl -
Subject digest:
fe024f56a16aa1168ea091c069c77352ca8f963f994545db42a6ce3bc0eb2dc3 - Sigstore transparency entry: 1652300938
- Sigstore integration time:
-
Permalink:
vshulcz/injex@0210662a1eeed793a018ac49a0ccaebbaee250ba -
Branch / Tag:
refs/tags/v1.2.1 - Owner: https://github.com/vshulcz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0210662a1eeed793a018ac49a0ccaebbaee250ba -
Trigger Event:
release
-
Statement type: