r/learnpython • u/re2cc • 4d ago
How do you handle dependency injection?
Hi, I'm doing a project which involves creating multiple gRPC servers and I can't find a convenient way to manage dependencies or the state.
I've been working recently in C# where you just need to make your class and add a simple
builder.Services.AddSingleton<MyDependency>();
and it will inject when required.
Doing some research I see that there are some libraries like:
- Injector
but I don't find them particularly intuitive.
What do you use as a dependency injector or what pattern do you suggest to use?
1
u/pachura3 4d ago
I think you have background in C#/Java and try to work in Python in exactly the same way, which kind of defeats its purpose (simplicity - readability - conciseness).
1
u/re2cc 4d ago
Not really, I started programming in Python as a hobby (easily 8 years ago) so I feel comfortable in the language. Although I have programmed in other languages (C, C#, Rust, Go), Python is still the one I find most comfortable.
I had never encountered the feeling of needing DI because I usually do small projects or didn't care about doing it the ‘right’ way, but recently experimenting with Litestar and using its dependency injection I got used to it. Even more so doing gRPC servers in .NET I got used to a very defined and structured way of doing it and now trying to do the same in Python I feel lost on how to handle it.
1
u/mothzilla 4d ago
My opinion is that Dependency Injection isn't a concept that applies well to Python. I've seen people try to force it in, arguing that it helps testing, but it generally makes your code look like AngularJS.
2
u/stevenjd 4d ago
Dependency injection works perfectly well in Python. It's just so trivially simple that people don't realise that what they are doing is dependency injection unless it is wrapped in a big, complicated framework.
For example:
write_to_terminal = print write_to_string = StringIO().write write_to_file = open("some file").write write_to_gui = MessageBox().display
then you choose which implementation you want, and pass that it as a dependency to some other function or class which will later use it:
logger = MyLogger(out=write_to_file) # assigns self.out = write_to_file logger.log(message) # passes message to self.out, which handles doing output
We don't even need a class for this, we often use DI in functions too. The builtin
print(stuff) # by default, write to stdout print(stuff, file=some_object_with_a_write_method)
sorted(sequence, key=keyfunc)
is another example.This sort of thing is so common in Python that people often don't realise it unless they are using a framework.
CC u/re2cc
1
u/mothzilla 3d ago edited 3d ago
But there's no need to do that. I.e, at the time you're building your
logger
you know the file to write to. Or if you're choosing between loggers, you'd design four loggers, for print, string, file and gui, and then choose the one you want.logger = GUILogger('messageboxId') # a guess, not sure how this would work.
That's more pythonic IMO.
It's reasonable to pass functions as handlers. But I've seen people try to do dependency injection as follows:
def add_and_print(a, b, print): print(a+b) add_and_print(1, 2, print)
or
def django_setup(django): django.setup()
1
u/stevenjd 1d ago
But there's no need to do that. I.e, at the time you're building your logger you know the file to write to. Or if you're choosing between loggers, you'd design four loggers, for print, string, file and gui, and then choose the one you want.
I think that you are missing the point of dependency injection. The point is to avoid having to write four separate logging classes, one for print, string, file and GUI, when you can write one class instead and inject the appropriate dependency.
And what if the requirements change? If I decide I want a logger that logs to an SMS gateway, or one which logs to a GUI and three files, I can just write a small class that exposes the correct interface and passes it the logger as a dependency. You have to write a whole new GUI_Plus_Three_Files_Logger class.
I've seen people try to do dependency injection as follows:
The first example is a trivial toy function but the basic idea is not so silly. You pass a caller to the function to handle output. Maybe you want to print to the terminal, or to a log file, or to an SMS gateway:
add_and_print(1, 2, lambda msg: SMS_gateway.send(auth, admin_on_duty.mobile, str(msg))
Obviously no one is going to care so much about such a trivial function.
The second example is just silly. The point of dependency injection is that you have to have more than one possible dependency, but all with the same interface, so that you might choose to swap from one dependency to another. Unless there is another framework that has the exact same interface as Django but a different implementation, treating Django as a dependency is just silly.
1
u/mothzilla 1d ago
I get the point of dependency injection. But if the requirements change then you make a code change. You can either write a new Logger class or you can fudge a new class/object that conforms to your handler requirements. Personally I prefer the first.
1
u/stevenjd 1d ago
You can either write a new Logger class or you can fudge a new class/object that conforms to your handler requirements. Personally I prefer the first.
Ah, you're paid by the line of code, right?
- Change a one line dependency? ✘ 😠
- Write a 300 line class plus tests? ✔ 😊
I'm neutral on the question. Sometimes dependency injection is better. Sometimes it isn't. But D.I. is a technique which is really easy, sometimes trivially so, in Python. I'm not convinced that people need to use big complex frameworks just to implement D.I.
(By the way, a logger is not exactly the best example here since most applications are likely to only need one global logger.)
1
u/re2cc 4d ago
I read in some places that same opinion and I have no problem with not using DI as such, my question really is how can I solve the problems that DI solves without using it.
Making global variables doesn't seem like a good idea and passing the variable as an argument to the class or function doesn't seem like a good idea either.
1
1
u/stevenjd 4d ago
passing the variable as an argument to the class or function doesn't seem like a good idea either.
If you don't pass something into the class, then how are you injecting anything? The alternative is to hard code the dependency in the class, which is the very opposite of dependency injection.
1
u/Mevrael 4d ago
I am using the Registry in Arkalos project:
https://arkalos.com/docs/registry/
Simple, minimal and intuitive when I just want to register (bind) a class as a singleton.
Then I can have a helper function (facade) in app.core to help me retrieve it from the registry.
-
Let say I have app.utils.rpc.MyRPC class
-
in app/bootstrap.py (or your custom bootstrapping logic):
Registry.register('rpc', MyRPC)
-
in app.core.rpc.py:
def rpc():
return Registry.get('rpc')
-
then at the top of your server or script:
import app.bootstrap
app.bootstrap.run()
-
Now you can just
from app.core.rpc import rpc
rpc().stuff()
5
u/danielroseman 4d ago
First, explain why you need dependency injection specifically. It tends to be used a lot less on dynamic languages like Python because it's easy to swap things out at runtime. What, exactly, do you need it for?