In the world of web development, managing dependencies efficiently is crucial for building scalable and maintainable applications. One powerful design pattern that helps achieve this is Dependency Injection (DI). DI simplifies the management of components and services in your application, making it easier to replace or extend functionality without modifying existing code. In Python web services, particularly those built with Flask, Dependency Injection can often seem like an unnecessary complexity. However, when used correctly, DI helps keep your codebase clean, modular, and testable. This is particularly important as your application grows, requiring more services and components to interact with each other. In this guide, we’ll explore how to implement a minimalist version of Dependency Injection in Flask-based Python web services. We’ll walk through the process of injecting classes and variables into your Flask application in a simple yet effective way, avoiding the overhead of heavyweight DI frameworks. By the end of this post, you’ll have the tools to build more maintainable and flexible Flask web services using this essential pattern.

Use Case
In traditional Flask-based Python web services, managing configuration variables and classes like a logger can quickly become repetitive and cumbersome. Without Dependency Injection (DI), we would need to initialize these components manually in each module where they are used.
Let’s look at a simple example:
- In the absence of DI, we would have to call os.getenv() for every variable we need in every module.
- Likewise, every module would manually initialize a Logger class, ensuring consistency and configuration in each instance.
Instead, by using DI, we can simplify this process. We initialize the environment variables and the logger class once—typically in a container—and then inject them into the relevant services or modules. This eliminates the need to manually call os.getenv() or create new logger instances throughout the codebase. This reduces redundancy and improves maintainability by centralizing the configuration and logger initialization in one place, making our code cleaner and more modular.
Dependent Class
First, create a logger.py file under the src/commons directory. During initialization, it will accept a log level as an integer and store it. Then, in each logging method, it will check whether the action meets the required level based on the log level provided during initialization.
import logging from datetime import datetime # pylint: disable=broad-except class Logger: def __init__(self, log_level: int): self.log_level = log_level if self.log_level == logging.DEBUG: logging.basicConfig(level=logging.DEBUG) def info(self, message): if self.log_level <= logging.INFO: self.dump_log(f"{message}") def debug(self, message): if self.log_level <= logging.DEBUG: self.dump_log(f"{message}") def warn(self, message): if self.log_level <= logging.WARNING: self.dump_log(f"{message}") def error(self, message): if self.log_level <= logging.ERROR: self.dump_log(f"{message}") def critical(self, message): if self.log_level <= logging.CRITICAL: self.dump_log(f"{message}") def dump_log(self, message): print(f"{str(datetime.now())[2:-7]} - {message}")
Next, create a variables.py file under the src/dependencies directory. This file will be responsible for loading the necessary variables from the environment.
# built-in dependencies import os # 3rd party dependencies from dotenv import load_dotenv load_dotenv() # load env vars from .env file class Variables: def __init__(self): self.log_level = int(os.getenv("LOG_LEVEL", "20"))
Third, create a container.py file under the src/dependencies directory. This file will be responsible for loading the necessary classes and modules, which will be injected into your services later.
# project dependencies from modules.core.service import CoreService from dependencies.variables import Variables from commons.logger import Logger class Container: def __init__(self, variables: Variables): self.variables = variables # initialize logger logger = Logger(log_level = variables.log_level) # initialize core service self.core_service = CoreService(logger = logger)
As you can see, the Logger class is initialized in the container.py file, while the log level information is passed from the variables. This log level is retrieved from the environment variables, ensuring that the logger is configured based on the environment-specific settings. We also stored variables in our container as well.
Next, create the service functionalities in the src/modules/core/service.py file. This service will retrieve the container during its initialization, and its welcome method will log a “Homepage called” message using the logger.
# project dependencies from commons.logger import Logger class CoreService: def __init__(self, logger: Logger): self.logger = logger def welcome(self): self.logger.info("homepage called") return "Welcome Home"
Now, let’s create the service endpoints in the src/modules/core/routes.py file.
# 3rd party dependencies from flask import Blueprint, request # project dependencies from dependencies.container import Container # initializations blueprint = Blueprint("routes", __name__) @blueprint.route("/") def home(): # container was initialized in app initialization once, inject it here container: Container = blueprint.container return container.core_service.welcome()
Here, we retrieved the container from blueprint but we haven’t stored the container into blueprint yet. We will do this while creating the app.
Let’s create the application in the src/app.py file. As you can see, we first create the variables object, which loads the necessary variables from the environment. Then, we initialize the container while passing the variables to it. Finally, we store the container into the blueprint, completing the dependency injection process.
# 3rd party dependencies from flask import Flask from flask_cors import CORS # project dependencies from modules.core.routes import blueprint as core_blueprint from dependencies.variables import Variables from dependencies.container import Container def create_app(): app = Flask(__name__) CORS(app) core_blueprint.container = Container(variables=Variables()) app.register_blueprint(core_blueprint) return app
To run our service, we will run the following shell script. This will get the application up at localhost’s 5000 port.
cd src gunicorn --workers=8 --timeout=3600 --bind=0.0.0.0:5000 "app:create_app()"
When the localhost:5000/ endpoint is called, it will log the message using the logger we injected.
More Dependencies
As the project grows and we need to log messages in different modules, we won’t need to initialize the logger again. Instead, we will simply use it from the injected container, ensuring consistent logging across the entire application. Let’s expand the project to understand better.
Create a healthcheck service as service.py under src/modules/health. In this service, we will log a “Healthcheck service called” message using the logger available in the container. Note that the logger was initialized in the application and injected into the container, making it accessible here.
# project dependencies from commons.logger import Logger class HealthService: def __init__(self, logger: Logger): self.logger = logger def is_healthy(self): self.logger.info("healthcheck service called") return "Health check: OK", 200
Similar to injection of logger in core service, we will initialize health service once in our container and inject logger to it here.
# project dependencies from modules.core.service import CoreService from modules.health.service import HealthService from dependencies.variables import Variables from commons.logger import Logger class Container: def __init__(self, variables: Variables): self.variables = variables # initialize logger logger = Logger(log_level = variables.log_level) # initialize core service self.core_service = CoreService(logger = logger) # initialize health service self.health_service = HealthService(logger = logger)
Now, create its endpoint under src/modules/health/routes.py. Health service was already initialized in our container and logger was passed to it already. So, we will use health service from container in routes instead of initializing it from scratch.
# 3rd party dependencies from flask import Blueprint, request # project dependencies from dependencies.container import Container # initializations blueprint = Blueprint("routes", __name__) @blueprint.route("/health") def is_healthy(): # container was initialized in app initialization once, inject it here container: Container = blueprint.container return container.health_service.is_healthy()
Finally, we will add health check endpoint’s blueprint into our existing application at src/app.py.
# 3rd party dependencies from flask import Flask from flask_cors import CORS # project dependencies from modules.core.routes import blueprint as core_blueprint from modules.health.routes import blueprint as health_blueprint from dependencies.variables import Variables from dependencies.container import Container def create_app(): app = Flask(__name__) CORS(app) core_blueprint.container = Container(variables=Variables()) health_blueprint.container = Container(variables=Variables()) app.register_blueprint(core_blueprint) app.register_blueprint(health_blueprint) return app
As you can see, the Logger class is initialized once in container.py, but it is used in both src/modules/core/service.py and src/modules/health/service.py. This is possible because we injected the initialized container into our application, making the logger accessible throughout the application. Similarly, we can load the necessary variables from the environment variables once in this manner, making them accessible throughout the application without needing to load them repeatedly in each module.
Although we’ve demonstrated a basic implementation of dependency injection, it can be easily expanded to suit more complex needs. You can use the framework outlined in this post as a foundation for your own project, adapting it as necessary.
Conclusion
In conclusion, by implementing Dependency Injection (DI) in our Flask-based Python web service, we’ve simplified the process of managing environment variables and logging. Through DI, we centralized the initialization of crucial components like the logger and environment variables, making them easily accessible throughout the application. As the project expands, this approach ensures maintainability and scalability, as we no longer need to repeatedly initialize or configure these components in every module. DI helps streamline the architecture, reduce redundancy, and foster cleaner, more modular code—making it an essential practice for larger applications.
Support this blog if you do like!