Framework
ets-labs/python-dependency-injector avatar
ets-labs/python-dependency-injector

Dependency Injector: a container-based DI framework for Python applications

Dependency injection framework for Python

4,913 stars353 forksPythonBSD-3-Clause

At a glance

What is it?
Dependency Injector organises object assembly into declarative containers and injects dependencies into functions and methods. It suits teams wiring Flask, FastAPI or asyncio applications who want providers and overrides rather than hand-rolled factories.
Who is it for?
Adopt Dependency Injector when your application already has a composition root worth naming and you need overrides for tests, dev and stage. Skip it for a ten-file script where a module-level function returning a client is enough.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 29 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Dependency Injector addresses in a Python codebase

Python makes it easy to import a module and call it. That convenience is exactly what turns a small service into a graph of module-level globals: a database session created at import time, a client reading an environment variable at import time, a logger configured somewhere in a package __init__. Tests then have to monkeypatch attributes on modules, and the patch target moves whenever someone renames a file.

Dependency Injector targets that situation. The README describes it as a dependency injection framework that helps implement the dependency injection principle, and it consolidates object assembly in a container so that injections are defined explicitly. The audience is application developers who want the wiring of their objects in one readable place, and who want to swap an API client for a stub without editing the code that uses it.

The library is not a service locator bolted onto an existing app. You declare what depends on what, then ask the container for the top-level object. Everything below it is constructed on demand.

Providers, containers and wiring: how the mechanism actually fits together

Three concepts carry the design. Providers describe how to build one thing. Containers group providers and let them reference each other. Wiring pushes container contents into function signatures.

The README lists the provider set: Factory, Singleton, Callable, Coroutine, Object, List, Dict, Configuration, Resource, Dependency and Selector. A Factory builds a new object per call. A Singleton builds once and returns the same instance. Configuration reads from yaml, ini and json files, pydantic settings, environment variables and plain dictionaries. Resource handles things with a lifecycle, such as a connection pool or an event loop.

Containers come in two shapes. DeclarativeContainer is a class whose attributes are providers; DynamicContainer is assembled at runtime. Providers inside a declarative container can reference each other by attribute, which is how a Service provider receives an api_client provider without either knowing about the other's construction.

Wiring is the part that changes how call sites look. The @inject decorator on a function, combined with a default value of Provide[Container.service], tells the framework to resolve that argument from the container at call time. The README states that wiring helps integrate with Django, Flask, Aiohttp, Sanic and FastAPI. The container needs container.wire(modules=[...]) before those functions are called.

Overriding is the fourth mechanism, and it is the one that pays for the rest. container.api_client.override(mock.Mock()) used as a context manager replaces the provider for the duration of the block. The README's own example calls main() twice, once with the real client and once inside the override block, and notes that the mock is injected the second time. The same trick covers dev and stage environments where you want a stub instead of a live dependency.

Installing Dependency Injector and wiring a first container

The package is on PyPI. The README gives a single install command:

bash
pip install dependency-injector

That pulls a wheel built from Cython sources. The build backend in pyproject.toml requires setuptools and Cython>=3.1.4, and setup.py cythonizes src/**/*.pyx. On platforms where no wheel is published, pip compiles the extension locally, which means you need a C compiler. The Makefile exposes a build target that runs python setup.py build_ext --inplace for that case.

A first container is short. The README's example defines a Configuration provider, a Singleton for the API client and a Factory for the service, then reads two values from the environment:

python
class Container(containers.DeclarativeContainer):
    config = providers.Configuration()
    api_client = providers.Singleton(ApiClient, api_key=config.api_key, timeout=config.timeout)
    service = providers.Factory(Service, api_client=api_client)

At startup the container is instantiated, the configuration is populated from environment variables, and the module is wired:

python
container = Container()
container.config.api_key.from_env("API_KEY", required=True)
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
container.wire(modules=[__name__])

from_env with required=True raises if API_KEY is missing, and as_=int converts TIMEOUT to an integer with a default of 5 when the variable is absent. After wire() runs, a function decorated with @inject and a Provide[Container.service] default receives the assembled Service. You should see no import-time construction of ApiClient: the Singleton is built the first time something asks for it.

The examples directory in the repository groups runnable code by topic: examples/containers/, examples/providers/, examples/wiring/, examples/miniapps/ and examples/demo/. Those are the files to read after the README, because they show the same providers in combinations the README does not.

Where Dependency Injector gets in the way

The Cython extension is the first constraint. Because the runtime is compiled, debugging steps through generated C rather than Python, and the setup.py build supports a DEPENDENCY_INJECTOR_DEBUG_MODE=1 environment variable that turns on profiling and line tracing with CYTHON_TRACE macros. That mode disables the limited API build, so a debug build and a portable wheel are mutually exclusive. If you need to read tracebacks inside the container internals, expect to build from source.

Wiring has a specific failure mode. container.wire(modules=[...]) must run before any injected function is called, and it must be told about every module whose functions use @inject. Miss a module and the Provide default resolves to a provider object rather than the dependency, or the call fails. This is also the source of the circular import questions people ask about: a module that imports the container while the container's providers import that module's classes creates a cycle. The usual fix is to keep container definitions in a module that imports nothing from the application layer.

Override is scoped, not global. Outside the with block, the original provider is back. Code that caches a resolved dependency in a module-level variable will keep the real object across an override, which defeats the test.

Finally, this is the wrong tool for a script with three functions. A container adds a layer of indirection that only pays off when there are several implementations to swap or several environments to configure. For a single entry point with no tests that need substitution, a plain function that constructs its dependencies is shorter and easier to follow.

Dependency Injector compared with Injector and with plain constructor arguments

Injector is the other Python DI library people name in the same breath. The approaches differ in a way that matters at the call site. Injector binds interfaces to implementations in a Module and resolves by type annotation, so a function declares what it wants and the framework finds it. Dependency Injector resolves by provider reference: you write Provide[Container.service], naming the container attribute rather than the type. That makes the container the source of truth and lets two providers of the same type coexist, which type-based resolution cannot express without qualifiers.

Against plain constructor arguments, the difference is where the assembly lives. Passing dependencies by hand keeps every dependency visible in the signature and needs no framework, but the wiring code spreads across every entry point. Dependency Injector moves it into one container and lets @inject pull values in. The cost is that the signature no longer shows the full dependency list, and readers have to open the container to see what Provide[Container.service] will build.

The Configuration provider is a third point of comparison. Reading environment variables through config.api_key.from_env(...) centralises parsing and type conversion, including the as_ and default arguments, in the container rather than scattering os.environ lookups through the codebase.

Maintenance, releases and what the BSD-3-Clause licence means here

The repository is not archived, and the last push was on 2026-09-01. Releases are frequent: 4.49.1 on 2026-06-18, 4.49.0 on 2026-03-22 and 4.48.3 on 2025-12-04. The project classifies itself as Development Status :: 5 - Production/Stable and requires Python >=3.8, with classifiers through Python 3.14 and support for both CPython and PyPy.

Upgrade cost is mostly about the compiled extension. A version bump can mean a new Cython build, so CI that installs from source will recompile, and the Makefile's DEPENDENCY_INJECTOR_LIMITED_API=1 path only applies on CPython 3.10 and later without the GIL disabled. If your deployment pins wheels for a specific platform, check that the release you want has one before scheduling the upgrade.

The licence is BSD-3-Clause, declared in pyproject.toml as a file reference to LICENSE.rst. That is a permissive licence: it allows use in closed-source products provided the copyright notice and disclaimer are retained. It is not a copyleft licence, so it does not require you to publish modifications. This is a description of the licence text, not legal advice; if your organisation has a licence review process, run the actual LICENSE.rst through it.

Editorial conclusion

Adopt Dependency Injector when your application already has a composition root worth naming and you need overrides for tests, dev and stage. Skip it for a ten-file script where a module-level function returning a client is enough. Before committing, verify that the providers you need exist for your Python version, that a Cython wheel is published for your platform, and that the wiring integration for your web framework is documented.

Frequently asked questions

What is the best way to manage Python dependencies?

Dependency Injector does not manage package dependencies; it manages object dependencies. It assembles your application's objects in a container and injects them into functions, while the package itself is installed from PyPI with pip install dependency-injector.

What is dependency injection and why is it used?

Dependency injection is the principle the README says this framework helps implement: instead of an object constructing what it needs, the dependency is supplied from outside. The README states that with Dependency Injector object assembling is consolidated in a container and injections are defined explicitly, which makes it easier to understand and change how an application works.

What is depends() in Python?

There is no depends() function in Dependency Injector. The framework resolves dependencies through providers and the @inject decorator with Provide[Container.service] defaults, or by calling the container directly.

How can I check my Python dependencies?

Dependency Injector does not audit installed packages. What it lets you inspect is your own object graph: providers are declared as container attributes such as api_client and service, so reading the container class shows what will be built and what each provider depends on.

Official sources

  1. ets-labs/python-dependency-injector on GitHub
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ets-labs-python-dependency-injector.svg)](https://hysenlabs.com/projects/ets-labs-python-dependency-injector)