Skip to main content

Tiny typed dependency injection for Python apps

Project description

Injex

Build Status pypi Docs Coverage Python Versions License

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 with block.
  • 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

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:

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


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.2.1.tar.gz (18.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.2.1-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

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

Hashes for injex-1.2.1.tar.gz
Algorithm Hash digest
SHA256 8619217d62640e29edf8d66e7dc65690479ee939e01004a08a566d6082704a0a
MD5 49332dcd13f1625a3d452aca909d92d8
BLAKE2b-256 6e665019c75e0a0fed8d5500297d3df1acba4a09eb805bda20e0aa6b2436492e

See more details on using hashes here.

Provenance

The following attestation bundles were made for injex-1.2.1.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.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

Hashes for injex-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fe024f56a16aa1168ea091c069c77352ca8f963f994545db42a6ce3bc0eb2dc3
MD5 254f5447f2f2e89726b1da3d02d5087f
BLAKE2b-256 795308279c3053ccdd372f28c018756f5b9e539fbc25f5ec381e3e97dac5a050

See more details on using hashes here.

Provenance

The following attestation bundles were made for injex-1.2.1-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