Skip to main content

Tiny typed dependency injection for Python apps

Project description

Injex

Build PyPI Downloads Coverage Python License

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.

Injex validates the dependency graph before startup

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-injector is 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

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

injex-1.5.0.tar.gz (38.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

injex-1.5.0-py3-none-any.whl (21.0 kB view details)

Uploaded Python 3

File details

Details for the file injex-1.5.0.tar.gz.

File metadata

  • Download URL: injex-1.5.0.tar.gz
  • Upload date:
  • Size: 38.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for injex-1.5.0.tar.gz
Algorithm Hash digest
SHA256 cf4bbde0aec1d743ee9fbaea61998d43affedea4396267b3e4aba7e8e9eb1687
MD5 9d9bd29ed2bc1566858aebccb8ab1489
BLAKE2b-256 6311891c1b5944e37bb051e6417e2865f503e62cd7e4814f25d01cf01a7f2c72

See more details on using hashes here.

Provenance

The following attestation bundles were made for injex-1.5.0.tar.gz:

Publisher: release.yml on vshulcz/injex

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file injex-1.5.0-py3-none-any.whl.

File metadata

  • Download URL: injex-1.5.0-py3-none-any.whl
  • Upload date:
  • Size: 21.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for injex-1.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 df6b90d2d06bae57fdef2674c3020bc00ebc4953820d094c86949dc66bf79abe
MD5 3994832c076eb4c9d476e40082acb978
BLAKE2b-256 2a61cf516295c2ee242824e5ca8886ec34c1cf8c5f963ba054d27491bea9b131

See more details on using hashes here.

Provenance

The following attestation bundles were made for injex-1.5.0-py3-none-any.whl:

Publisher: release.yml on vshulcz/injex

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page