Tiny typed dependency injection container for Python
Project description
Injex
Tiny typed dependency injection for Python that catches missing dependencies and cycles before your app starts — with zero runtime dependencies.
You wire one service graph at startup, validate it in a single call, then reuse it from FastAPI, Typer, workers, scripts, and tests. Application classes stay plain: normal constructors, normal type hints, no decorators.
pip install injex
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() # fail fast if the graph is incomplete
container.resolve(RegisterUser).execute("ada@example.com")
What makes it different
Most small DI containers stop at "resolve a graph." Injex's distinctive feature is that it can check the whole graph without constructing anything, so missing registrations, missing annotations, and cycles surface at startup or in CI — not on the first request or background job.
errors = container.validate() # list of problems, nothing constructed
container.assert_valid() # or raise with all of them at once
That makes it safe to run as a startup guard even when real constructors open sockets or files.
Lifetimes, overrides, scopes
container.add_singleton(ApiClient) # one instance for the app lifetime
container.add_transient(UseCase) # a new instance per resolve
container.add_scoped(RequestContext) # one instance per scope (request/job)
# Swap a dependency inside a test, restored automatically on exit:
with container.override(EmailSender, instance=fake_sender):
container.resolve(RegisterUser).execute("test@example.com")
See the tutorial for factories, named registrations,
resolve_all(), optional dependencies, and property injection.
When to use it
- A service layer reused by an API, CLI, worker, and tests, where copy-pasted wiring drifts out of sync.
- You want a missing or cyclic dependency to fail at startup, not at 3 AM.
- Tests should replace one real service with a fake without touching production wiring.
When not to: a handful of constructor calls in one entrypoint is clearer with plain manual wiring — reach for Injex when that wiring starts repeating.
Async
Injex resolves async dependencies too. Register an async def factory or an
async-generator resource and resolve it through aresolve() / ascope():
async def db_session(settings: Settings): # async-generator resource
pool = await open_pool(settings.database_url)
try:
yield pool
finally:
await pool.aclose() # finalized when the scope exits
container.add_scoped_factory(Pool, db_session)
async with container.ascope() as scope:
pool = await scope.aresolve(Pool)
Resources are finalized LIFO via the standard library's AsyncExitStack (still
zero runtime deps). The sync resolve() raises AsyncResolutionRequiredException
if the graph needs async work, so you never silently get an un-awaited object. See
async resolution and the
FastAPI example.
Where it doesn't fit (yet)
- No provider/config DSL. If you want a rich configuration-injection system,
dependency-injectoris a better fit. - No deep framework auto-wiring. Injex owns the graph; FastAPI/Typer adapt it at their edge — it won't inject into route signatures for you.
Performance
Injex compiles and caches a flat creator per service graph. On a small synthetic graph (singleton config + client, transient repository/service/use-case) it resolves faster than several popular containers on the same machine:
| Library | Median resolve time |
|---|---|
| manual wiring | 0.266 µs/op |
| Injex | 0.333 µs/op |
| dishka | 0.786 µs/op |
| Wireup, same scope | 0.872 µs/op |
| dependency-injector | 1.709 µs/op |
| lagom | 9.487 µs/op |
| punq | 56.982 µs/op |
This is synthetic and graph-specific — not a universal ranking. Reproduce it:
uv run --with punq --with lagom --with dependency-injector --with wireup --with dishka \
python benchmarks/resolve_graph.py
See performance notes for the full table and method.
How it fits
One validated graph at the composition root; every entrypoint resolves from it.
flowchart LR
subgraph root["Composition root — one validated graph"]
direction LR
S[Settings] --> C[ApiClient]
C --> R[UserRepository]
C --> E[EmailSender]
R --> U[RegisterUser]
E --> U
end
root --> API[FastAPI]
root --> CLI[Typer CLI]
root --> WK[Worker]
root --> TS[Tests]
How it compares
| Feature | Injex | dependency-injector | punq | lagom |
|---|---|---|---|---|
| Zero runtime dependencies | ✅ | ❌ | ✅ | ✅ |
| Type-hint constructor injection | ✅ | ✅ | ✅ | ✅ |
| Singleton / transient / scoped | ✅ | ✅ | partial | ✅ |
| Named registrations | ✅ | ✅ | ❌ | ✅ |
| Property injection | ✅ | ❌ | ❌ | ❌ |
| Temporary test overrides | ✅ | ✅ | ❌ | ✅ |
| Graph validation without constructing services | ✅ | ❌ | ❌ | ❌ |
For a deeper, fair comparison see Injex vs other DI options.
API at a glance
| Method | Use when |
|---|---|
add_singleton(T, Impl) |
One instance reused for the app lifetime. |
add_transient(T, Impl) |
A new instance on every resolve. |
add_scoped(T, Impl) |
One instance reused inside one scope. |
add_*_factory(T, factory) |
Construction needs custom code. |
add_instance(T, instance) |
You already have the object. |
resolve(T) / resolve_all(T) |
Resolve one, or all unnamed implementations. |
create_scope() |
Start a request, job, or message lifetime. |
override(T, ...) |
Temporarily replace a dependency in tests. |
validate() / assert_valid() |
Check wiring before startup. |
Documentation
- Docs site · Tutorial · API reference
- Validation guide · Comparison · vs FastAPI Depends
- Recipes · Migrating from a factories module · Performance
- Examples: clean architecture, FastAPI lifespan, CLI, testing, scopes
Contributing
Contributions are welcome when they keep the API small, tested, and dependency-free. Useful changes usually improve documentation, typing, examples, or narrow edge cases. See CONTRIBUTING.md.
Thanks to Muhammad Saqib Atif, mahek, oppnc, and YuuGR1337 for improving Injex.
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.5.1.tar.gz.
File metadata
- Download URL: injex-1.5.1.tar.gz
- Upload date:
- Size: 40.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
975971197a2f67cfc1d60f66640126d9613aaec17946f76ec2afe75a6e9e0797
|
|
| MD5 |
024ae3703c5db5c8f7c95b604d3c7d73
|
|
| BLAKE2b-256 |
b24a18a097effc7afeb7f0bae6c6623b5d0e5c0c4c4f7ae96e5259c5ac1ee8c0
|
Provenance
The following attestation bundles were made for injex-1.5.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.5.1.tar.gz -
Subject digest:
975971197a2f67cfc1d60f66640126d9613aaec17946f76ec2afe75a6e9e0797 - Sigstore transparency entry: 1868564262
- Sigstore integration time:
-
Permalink:
vshulcz/injex@a9f6390c921bc782b45606aa0d2f9713e516591a -
Branch / Tag:
refs/tags/v1.5.1 - Owner: https://github.com/vshulcz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9f6390c921bc782b45606aa0d2f9713e516591a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file injex-1.5.1-py3-none-any.whl.
File metadata
- Download URL: injex-1.5.1-py3-none-any.whl
- Upload date:
- Size: 21.5 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 |
2615e85fe84ff7a0fabe9fd4ae6456354e8ebf637feae6d7b2260e1ee31cf4a7
|
|
| MD5 |
b8877020e3e214a7b59d9282e92fda8d
|
|
| BLAKE2b-256 |
804dc49bc6d5e93a5222a3cb3b7825dccd7514c56177bb31f6beafae7bf89fcd
|
Provenance
The following attestation bundles were made for injex-1.5.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.5.1-py3-none-any.whl -
Subject digest:
2615e85fe84ff7a0fabe9fd4ae6456354e8ebf637feae6d7b2260e1ee31cf4a7 - Sigstore transparency entry: 1868564419
- Sigstore integration time:
-
Permalink:
vshulcz/injex@a9f6390c921bc782b45606aa0d2f9713e516591a -
Branch / Tag:
refs/tags/v1.5.1 - Owner: https://github.com/vshulcz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a9f6390c921bc782b45606aa0d2f9713e516591a -
Trigger Event:
workflow_dispatch
-
Statement type: