Dependency Injection in Python (DIY)

dboost.me
3 min readAug 8, 2023

--

Let’s think about dependency injection. In comparison with the java ecosystem, dependency injection is not commonly used in the python world.

Dependency injection is a cool technique allowing developers to not care about how the service or component get’s created — they just ask for it and DI framework will give it to them.

Good DI

Let’s illustrate this principal using Kotlin language and slightly enhanced google-juice library. Here is how we get an instance of HttpClient interface, which is returned by injector container.

interface HttpClient {
fun get(url: String, query_params_map: Map<String, String>)
}


// how to get an http client
val httpClient = injector.getInstance<HttpClient>()

DI container knows where the implementation is stored and how to created it. Here is the real implementation:

// real implementation
class HttpClientImpl(val awsCredentials: AwsCredentials): HttpClient {
....
}

We have two components — http client and was credentials, thus we need to declare how they should be created:

class HttpModule: AbstractModule() {

override fun configure() {
binder().bindTo<HttpClient, HttpClientImpl>()
binder().bindToProvider<AwsCredentials, EnvAwsCredentialsFactory>()
}
}

We are telling here that we are going to use HttpClientImple as HttpClient interface implementation andAwsCredentials will be provided by EnvAwsCredentialsFactory (which is going to read AWS credentials from the env variables).

With kotlin , java or any other statically typed language that looks good, feels good and works good. Which is not the case for dynamically typed languages, since we rely on types to inject the proper values.

Let’s take a look at how it can be implemented in python

My Python DI

Let’s play a game and create a by-name injector, where services will be injected by parameter name. Not very useful for any real world project, but a good learning experience.

Here we are going to build a weather forecast service. WeatherForecast service expects aTemperatureSensor and a WindSensor services in order to be instantiated.

class TemperatureSensor:
def get_temperature(self):
return 100.0


class WindSensor:
def get_wind_speed(self):
return 30.0


class WeatherForecast:
def __init__(self, temperature_sensor, wind_sensor):
self.temperature_sensor = temperature_sensor
self.wind_sensor = wind_sensor

def get_forecast(self):
result = []

if self.wind_sensor.get_wind_speed() < 10:
result.append("wind good")
else:
result.append("wind bad")

if self.temperature_sensor.get_temperature() > 30:
result.append("it's hot")
else:
result.append("it's not hot")

return result

Let’s create a DI container, consisting of two things:

  1. container with the dependencies
  2. service initialization function
container = {
"temperature_sensor": TemperatureSensor(),
"wind_sensor": WindSensor()
}


def get_instance(container, clazz):
param_names = {p for p in inspect.signature(clazz.__init__).parameters}.difference({"self"})
args = {}
for param_name in param_names:
args[param_name] = container.get(param_name)
return clazz(**args)


forecast = get_instance(container, WeatherForecast)
print(forecast.get_forecast())

In the given example WeatherForecast service was initiated and get_forecast function worked.

Conclusion

When DI is used everywhere in the project it may come very handy, implementation of a particular class can be replaced in a second, special configurations for tests and different environments can be customized, dependencies become structured and configurable.

Here we explored a DI concept a little bit by creating a toy container, not suitable for any production application. That is the way!

Do you use DI for your python projects?

--

--

No responses yet