
How I stopped fighting my Python code and finally I made it modular | By Saad Jamil | February 2025
Some time ago, I worked on a Python project It started as a simple scenario but quickly became an unmanageable system. Each function depended on another, creating an invisible network of dependencies.
At first, everything worked well. But as the project grew up:
- Tests have become a nightmare – The mockery was painful because the dependencies were buried deeply in the code.
- Refactoring has constantly broken things – A small change in part of the code has waved on the entire application.
- The scaling was almost impossible – New features meant rework the old ones and introduce unpredictable bugs.
I had created without knowing it A closely coupled system where everything was connected in a way that I had not planned.
This is where I learned on Injection of dependence (DI). At first, it looked like an overly designed architectural model used in massive business applications. But once I implemented it, I realized that it was exactly what I missed.
Content board
How does it work?
Automated dependence injection
· Facility
Management of dynamic arguments
1. Replace the dependencies to the execution
2. Partial injection
Using several containers
Example: modular services
When to use dependence injection?
Does the injection of dependence are worth it?
Most Python applications make this current error – Rigid coding dependencies in the classes.
The problem: hard -coded outbuildings
class Service:
def __init__(self):
self.db = Database() # Hardcoded dependencydef get_data(self):
return self.db.fetch_data()
Why is it a problem?
1 and 1 Well -coupled – changing Database()
requires modifying this class.
2 Difficult to test – You cannot easily replace Database()
with a simulation or an alternative.
The solution: inject dependencies manually
Instead of creating dependencies inside the class, transmit them from the outside:
class Service:
def __init__(self, db):
self.db = db # Injected dependencydef get_data(self):
return self.db.fetch_data()
Now we can Exchange dependencies without modifying the class::
db = Database()
service = Service(db) # Injecting the dependency
✔ Easier to test – You can go through simulated objects.
✔ More flexible – Works with different database backends.
✔ Scalable – THE Service
The class does not need to change when dependencies change.
This manual approach Works well for small applications. However, as an application develops, Dependencies management becomes manually tedious.
For larger applications, manually passing dependencies become painful. Imagine a electronic commerce system With several services:
- AuthService manages authentication.
- User service Manages user profiles.
- Payment service treats transactions.
Each service depends on different componentsAnd passing out the dependencies with each class quickly becomes unmanageable.
This is where the Dependent injector Simplifies things. Instead of cable everything manually together, We define a central container that manages dependencies for us.
First, install the package:
pip install dependency-injector
Then define a container To manage dependencies:
from dependency_injector import containers, providersclass Database:
def fetch_data(self):
return "data"
class Service:
def __init__(self, db):
self.db = db
def get_data(self):
return self.db.fetch_data()
# Define a DI container
class Container(containers.DeclarativeContainer):
db = providers.Singleton(Database)
service = providers.Factory(Service, db=db)
# Retrieve instances
container = Container()
service = container.service()
print(service.get_data()) # "data"
Now the injector of dependence automatically manages the creation of objects and the injection of dependence.
If you ever need to change Database()
to another implementation, you I don’t have to touch your service lessons– Simply update the container.