I would like to ask if there is a Pythonic way to do dependency injection. I know that most of the ansewrs will be depending on the project. Therefore, let's put a clear example: I am trying to compare a TypeScript structure with a Python structure, and yes, we are talking about backend development!
The structure for the backend is as follows: Route - Controller - Feature - Repository if needed a non Basemethod.
This structure is quite used in typescript, where the route calls the contoller, the controller does some sanity checks, then the controller calls the feature and finally, if needed the feature performs some action into the DB.
So I thought this would be a nice approach to extrapolate to a Python backend project. Nevertheless, when it comes to dependency injection, I am unsure on how to do it. In TypeScript, you have a index.ts where the class is initialized and the dependencies are injected.
For example, imagine we have a feature that perform an action in a custom database method defined in a custom repository. Your Feature would look like:
import SomeRepository from '...'
SomeFeature {
private someRepository: SomeRepository;
constructor(socialUsersRepository) {
this.someRepository = someRepository;
}
...
}
Then, in the same folder you have your feature, you will have an index.ts that initializes and inject the dependency:
import SomeRepository from '...'
import SomeFeature from '...'
export default new SomeFeature(SomeRepository);
I have been looking on how to do it in a Python project, and I have found some ways to do it. Nevertheless, I am unsure on which one is the best Pythonic or if there is some approach that should be avoided due to python conflicts.
We could perform the dependency injection in the first "layer", which will initialize everything in routes.
OPTION 1
from ... import SomeRepository
from ... import SomeFeature
from ... import SomeController
some_repository = SomeRepository()
some_feature = SomeFeature(some_repository)
some_controller = SomeController(some_feature)
@app.get("/")
async def some_endpoint():
return some_controller.main_function()
But this way seems a bit strange. I have looked into a Factory Method, which I believe is similar to the Typescript approach where you create a init.py where you initialize the feature such as:
OPTION 2
from ... import SomeRepository
from ... import SomeFeature
def create_delete_document_feature():
update_document_repository = SomeRepository()
return SomeFeature(some_repository)
Then, you can import create_delete_document_feature from this module, which somehow keeps the structure modular and centralized.
Also there is the option to initialize the repository in the constructor of the feature class, such as:
OPTION 3
from ... import SomeRepository
class SomeFeature:
def __init__(self):
self.some_repository = SomeRepository()
Additionally, if you are using FastAPI you can benefit from its dependency injection, where you define a dependecies.py and you define the functions which will initialize the classes, such as:
OPTION 4
from fastapi import Depends
from ... import SomeRepository
from ... import SomeFeature
from ... import SomeController
def get_some_repository():
return SomeRepository()
def get_some_feature(some_repository=Depends(get_some_repository)):
return SomeFeature(some_repository)
def get_some_controller(some_feature=Depends(get_some_feature)):
return SomeController(some_feature)
Nevertheless, this approach is creating new instances for each endpoint call, which differ from the TypeScript approach. Therefore, if a feature deals with some model loading it may cause a lot of delay in the resolution of thar feature. Which I think it could be solved using
use_cache=True
But I am not sure that this will be a 100% solution, or if I should instead manually initialize instances at application startup and then reuse them across requests.
Is someone familiar with backend development in Python and know which would be the best approach to follow?
use_cacheinDependsin FastAPI is True by default - but it's only relevant for that specific attempt at resolving the hierarchy (so if the same dependency is requested multiple times, it's only evaluated once unless you setuse_cache=False). If you want to make something only be loaded once per process, either initialize it as part of startup or as part of a lifespan function, or wrap the function call in afunctools.lru_cachedecorator to only have it evaluated once.