-2

I want to perform dependency injection in my Python application without using reflection. My position on reflection is informed largely by the "don't hide things" philosophy (I am unable to find a decent reference to this on google at the moment). What that basically means to me is that code ought to be easily discoverable by an incoming programmer (either your own code at some point in the future, or another programmer coming in to edit or extend your code).

It's important for me to note here that my primary motivation for dependency injection is the ability to easily test individual services and dependencies without using Python's patch, but instead to rely on an easily accumulated set of reusable mock dependencies.

Setting aside the reflection debate for the moment, here is one way I see that one can accomplish dependency injection without reflection.

The Container

The container houses my Python application's dependencies. It could be a simple dictionary.

# container.py

dependencies = {}

The Composition Root

The point of the composition root is to have a centralized location where all needed application dependencies are drawn together and configured in whatever way is necessary. Some classes, for example, might need to be instantiated with config-specific parameters. In Python, I have not seen something like this in the wild. It seems like the composition root needs to be imported specifically by an environment-specific, executed module.

The below illustrates an example where a hypothetical dependency TaskManager depends on a service ApiService. TaskManager, in turn, is a service used throughout the application.

# run_prod.py - runs a production-configured instance of my application

from container import dependencies
from services import TaskManager, ApiService
import config
from app import App


api_service = ApiService(config.api_service_endpoint)
task_manager = TaskManager(api_service)
dependencies['task_manager'] = task_manager

app = create_app(dependencies)
app.run(host=config.host, port=config.port)

The App

The application itself at this point is more or less a shallow orchestrator of the services injected into it, since all chunks of code that need to be tested in isolation are defined outside of it, and injected into it in this scheme.

# app.py

def create_app(dependencies)
    app = Flask(__name__)
    
    @app.route('/task/<task_id>', methods=['GET'])
    def get_task(task_id):
        task = dependencies.get('task_manager').find_task_by_id(task_id)
        return task

Another similar approach might be to import the dependency container, rather than inject it as an argument to the create_app function. In that case I don't need the constructor function:

# app.py

from container import dependencies

app = Flask(__name__)
    
@app.route('/task/<task_id>', methods=['GET'])
def get_task(task_id):
    task = dependencies.get('task_manager').find_task_by_id(task_id)
    return task

Testing as a primary motivation for dependency injection

We can create as many use-specific composition roots as we want. For example, if we create one for testing, instead of having a separate run_prod.py, we can just compose the dependencies in the same module as the test (or in a separate module, for re-use). Let's go with module test_flask_endpoint.py.

For this example, let's say we want to test the TaskManager together with the Flask layer, but don't want to make outgoing HTTP calls. We mock out ApiService.

# test_flask_endpoint.py
import pytest

@pytest.fixture
def test_app():
    from services import TaskManager

    class MockApiService():
        def get_task_details(task_id):
            {"task_id": task_id, "description": "does something special"}

    mock_api_service = MockApiService
    task_manager = TaskManager(mock_api_service)
    dependencies['task_manager'] = task_manager

    app = create_app(dependencies)
    test_app = app.test_client()
    return test_app


def test_task_endpoint(test_app):
    assert test_app.get('/task/3').json()['description'] == 'does something special'

One disadvantage I see to doing DI this way is that the app is coupled to the injector, namely, the dependencies argument of create_app. Is there a way around this? It is not clear to me that this matters.

2
  • Software engineering usually doesn't have clear-cut answers. The way you did this is a pretty standard dependency injection pattern. As you point out, you are now tied to the injector. Nov 25, 2020 at 19:00
  • 1
    You are basically implementing the Service Locator (Anti) Pattern. Nov 25, 2020 at 22:03

2 Answers 2

4

The reason your app is coupled to the injector is because you aren’t using Dependency Injection. You’re using a Service Locator.

With dependency injection the things receiving the injection don’t know about any container. They only know what they need.

That doesn’t mean you can’t use a container. You can. But you can only use it in the composition root. If you were doing that I wouldn’t see gets everywhere.

7
  • The important thing to remember with DI is that this is accomplished primarily by paying constructor arguments when initializing new objects. Nov 26, 2020 at 14:45
  • 1
    @GregBurghardt paying? You mean passing? Nov 26, 2020 at 18:03
  • Yup. Auto-correct fail. Should be "passing constructor arguments." Nov 26, 2020 at 18:32
  • Yes DI is just a fancy way to say reference passing. Python is no exception. It is easier then some languages because it has named arguments. Nov 26, 2020 at 18:35
  • @candied_orange How can we construct this such that consumer code consumes the container, without knowing about the container? This is the part I most want to know! Thanks.
    – vincent
    Nov 27, 2020 at 1:16
1

Python never needs dependency injection.

A module namespace is basically just syntax sugar for a dictionary, so just use a module as your composition root.

The most basic way to do DI in Python is to just use star-import to define your dependencies:

# myapp.py: the main "DI config" file

from api.config_base import * # inherit from the standard config of api
from services import FooTaskManager as TaskManager

# in theory, you can use conditional statements in the
# configuration file since it's just plain Python, but try
# to avoid overcomplicating it
if DEBUG:
    from services.debug import DebugTaskManager as TaskManager

task_manager = TaskManager(api_service)

# if you need sub-namespace and don't want to create a separate file, you can put it inside a class
class mysubsection:
    from blah import foo

Basically, just treat the global namespace module execution as a DI configuration language, and the DI configuration language is basically just plain old Python.

Other parts of the application that needs injected parts simply imports myapp and calls the appropriate methods, e.g. myapp.task_manager.foo(), while tests and any parts that needs a specific/non-injected implementation imports directly from the original module.

If you want to have multiple configurations, simply use multiple modules, e.g. myapp1 and myapp2, and inheritance between configurations can just use star import.

2
  • Even more lightweight. I love it! Thanks.
    – vincent
    Nov 25, 2020 at 21:32
  • This just feels like the service locator pattern by another name. The service locator is just called myapp now instead of dependencies. Nov 26, 2020 at 14:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.