Skip to content

Commit 81ab8f8

Browse files
committed
Update DI in Python docs page
1 parent 2b4d211 commit 81ab8f8

File tree

8 files changed

+244
-206
lines changed

8 files changed

+244
-206
lines changed

docs/introduction/di_in_python.rst

Lines changed: 239 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,255 @@
11
Dependency injection and inversion of control in Python
2-
-------------------------------------------------------
2+
=======================================================
33

44
.. meta::
5-
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control
6-
:description: This article describes benefits of dependency injection and
7-
inversion of control for Python applications. Also it
8-
contains some Python examples that show how dependency
9-
injection and inversion could be implemented. In addition, it
10-
demonstrates usage of dependency injection framework,
11-
IoC container and such popular design pattern as Factory.
12-
13-
History
14-
~~~~~~~
15-
16-
Originally, dependency injection pattern got popular in languages with static
17-
typing, like Java. Dependency injection framework can
18-
significantly improve flexibility of the language with static typing. Also,
19-
implementation of dependency injection framework for language with static
20-
typing is not something that one can do shortly, it could be quite complex
21-
thing to be done well.
22-
23-
While Python is very flexible interpreted language with dynamic typing, there
24-
is a meaning that dependency injection doesn't work for it as well, as it does
25-
for Java. Also there is a meaning that dependency injection framework is
26-
something that Python developer would not ever need, cause dependency injection
27-
could be implemented easily using language fundamentals.
28-
29-
Discussion
30-
~~~~~~~~~~
31-
32-
It is true.
33-
34-
Partly.
35-
36-
Dependency injection, as a software design pattern, has number of
37-
advantages that are common for each language (including Python):
38-
39-
+ Dependency Injection decreases coupling between a class and its dependency.
40-
+ Because dependency injection doesn't require any change in code behavior it
41-
can be applied to legacy code as a refactoring. The result is clients that
42-
are more independent and that are easier to unit test in isolation using
43-
stubs or mock objects that simulate other objects not under test. This ease
44-
of testing is often the first benefit noticed when using dependency
45-
injection.
46-
+ Dependency injection can be used to externalize a system's configuration
47-
details into configuration files allowing the system to be reconfigured
48-
without recompilation (rebuilding). Separate configurations can be written
49-
for different situations that require different implementations of
50-
components. This includes, but is not limited to, testing.
51-
+ Reduction of boilerplate code in the application objects since all work to
52-
initialize or set up dependencies is handled by a provider component.
53-
+ Dependency injection allows a client to remove all knowledge of a concrete
54-
implementation that it needs to use. This helps isolate the client from the
55-
impact of design changes and defects. It promotes reusability, testability
56-
and maintainability.
57-
+ Dependency injection allows a client the flexibility of being configurable.
58-
Only the client's behavior is fixed. The client may act on anything that
59-
supports the intrinsic interface the client expects.
60-
61-
.. note::
62-
63-
While improved testability is one the first benefits of using dependency
64-
injection, it could be easily overwhelmed by monkey-patching technique,
65-
that works absolutely great in Python (you can monkey-patch anything,
66-
anytime). At the same time, monkey-patching has nothing similar with
67-
other advantages defined above. Also monkey-patching technique is
68-
something that could be considered like too dirty to be used in production.
69-
70-
The complexity of dependency injection pattern implementation in Python is
71-
definitely quite lower than in other languages (even with dynamic typing).
72-
73-
.. note::
74-
75-
Low complexity of dependency injection pattern implementation in Python
76-
still means that some code should be written, reviewed, tested and
77-
supported.
78-
79-
Talking about inversion of control, it is a software design principle that
80-
also works for each programming language, not depending on its typing type.
81-
82-
Inversion of control is used to increase modularity of the program and make
83-
it extensible.
84-
85-
Main design purposes of using inversion of control are:
86-
87-
+ To decouple the execution of a task from implementation.
88-
+ To focus a module on the task it is designed for.
89-
+ To free modules from assumptions about how other systems do what they do and
90-
instead rely on contracts.
91-
+ To prevent side effects when replacing a module.
92-
93-
Example
94-
~~~~~~~
95-
96-
Let's go through next example:
97-
98-
.. image:: /images/miniapps/engines_cars/diagram.png
99-
:width: 100%
100-
:align: center
101-
102-
Listing of ``example.engines`` module:
103-
104-
.. literalinclude:: ../../examples/miniapps/engines_cars/example/engines.py
105-
:language: python
5+
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Example
6+
:description: This page describes a usage of the dependency injection pattern in Python. It
7+
contains Python examples that show how to implement dependency injection. It
8+
demonstrates a usage of the dependency injection framework
9+
Dependency Injector, its container, Factory, Singleton and Configuration
10+
providers. The example show how to use Dependency Injector providers overriding
11+
feature for testing or configuring project in different environments and explains
12+
why it's better then monkey-patching.
10613

107-
Listing of ``example.cars`` module:
14+
Originally dependency injection pattern got popular in the languages with a static typing,
15+
like Java. Dependency injection framework can significantly improve flexibility of the language
16+
with a static typing. Implementation of a dependency injection framework for a language
17+
with a static typing is not something that one can do quickly. It will be a quite complex thing
18+
to be done well. And will take time.
10819

109-
.. literalinclude:: ../../examples/miniapps/engines_cars/example/cars.py
110-
:language: python
20+
Python is an interpreted language with a dynamic typing. There is an opinion that dependency
21+
injection doesn't work for it as well as it does for Java. A lot of the flexibility is already
22+
built in. Also there is an opinion that a dependency injection framework is something that
23+
Python developer rarely needs. Python developers say that dependency injection can be implemented
24+
easily using language fundamentals.
11125

112-
Next example demonstrates creation of several cars with different engines:
26+
This page describes the advantages of the dependency injection usage in Python. It
27+
contains Python examples that show how to implement dependency injection. It demonstrates a usage
28+
of the dependency injection framework ``Dependency Injector``, its container, ``Factory``,
29+
``Singleton`` and ``Configuration`` providers. The example shows how to use ``Dependency Injector``
30+
providers overriding feature for testing or configuring project in different environments and
31+
explains why it's better then monkey-patching.
11332

114-
.. literalinclude:: ../../examples/miniapps/engines_cars/example_di.py
115-
:language: python
33+
What is dependency injection?
34+
-----------------------------
11635

117-
While previous example demonstrates advantages of dependency injection, there
118-
is a disadvantage demonstration as well - creation of car requires additional
119-
code for specification of dependencies. Nevertheless, this disadvantage could
120-
be easily avoided by using a dependency injection framework for creation of
121-
inversion of control container (IoC container).
36+
Let's see what the dependency injection is.
12237

123-
Example of creation of several inversion of control containers (IoC containers)
124-
using :doc:`Dependency Injector <../index>`:
38+
Dependency injection is a principle that helps to decrease coupling and increase cohesion.
12539

126-
.. literalinclude:: ../../examples/miniapps/engines_cars/example_ioc_containers.py
127-
:language: python
40+
.. image:: images/coupling-cohesion.png
41+
42+
What is coupling and cohesion?
43+
44+
Coupling and cohesion are about how tough the components are tied.
45+
46+
- **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way
47+
to disassemble.
48+
- **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and
49+
assemble back or assemble a different way. It is an opposite to high coupling.
50+
51+
When the cohesion is high the coupling is low.
52+
53+
Low coupling brings a flexibility. Your code becomes easier to change and test.
54+
55+
How to implement the dependency injection?
56+
57+
Objects do not create each other anymore. They provide a way to inject the dependencies instead.
58+
59+
Before:
60+
61+
.. code-block:: python
62+
63+
import os
64+
65+
66+
class ApiClient:
67+
68+
def __init__(self):
69+
self.api_key = os.getenv('API_KEY') # <-- the dependency
70+
self.timeout = os.getenv('TIMEOUT') # <-- the dependency
71+
72+
73+
class Service:
74+
75+
def __init__(self):
76+
self.api_client = ApiClient() # <-- the dependency
77+
78+
79+
if __name__ == '__main__':
80+
service = Service()
81+
82+
83+
After:
84+
85+
.. code-block:: python
86+
87+
import os
88+
89+
90+
class ApiClient:
91+
92+
def __init__(self, api_key: str, timeout: int):
93+
self.api_key = api_key # <-- the dependency is injected
94+
self.timeout = timeout # <-- the dependency is injected
95+
96+
97+
class Service:
98+
99+
def __init__(self, api_client: ApiClient):
100+
self.api_client = api_client # <-- the dependency is injected
101+
102+
103+
if __name__ == '__main__':
104+
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
105+
106+
``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a
107+
timeout from a configuration file or even get them from a database.
108+
109+
``Service`` is decoupled from the ``ApiClient``. It does not create it anymore. You can provide a
110+
stub or other compatible object.
111+
112+
Flexibility comes with a price.
113+
114+
Now you need to assemble the objects like this::
115+
116+
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
117+
118+
The assembly code might get duplicated and it'll become harder to change the application structure.
119+
120+
Here comes the ``Dependency Injector``.
121+
122+
What does the Dependency Injector do?
123+
-------------------------------------
124+
125+
With the dependency injection pattern objects lose the responsibility of assembling the
126+
dependencies. The ``Dependency Injector`` absorbs that responsibility.
127+
128+
``Dependency Injector`` helps to assemble the objects.
129+
130+
It provides a container and providers that help you with the objects assembly. When you
131+
need an object you get it from the container. The rest of the assembly work is done by the
132+
framework:
133+
134+
.. code-block:: python
135+
136+
from dependency_injector import containers, providers
137+
138+
139+
class Container(containers.DeclarativeContainer):
140+
141+
config = providers.Configuration()
142+
143+
api_client = providers.Singleton(
144+
ApiClient,
145+
api_key=config.api_key,
146+
timeout=config.timeout.as_int(),
147+
)
148+
149+
service = providers.Factory(
150+
Service,
151+
api_client=api_client,
152+
)
153+
154+
155+
if __name__ == '__main__':
156+
container = Container()
157+
container.config.api_key.from_env('API_KEY')
158+
container.config.timeout.from_env('TIMEOUT')
159+
160+
service = container.service()
161+
162+
Retrieving of the ``Service`` instance now is done like this::
163+
164+
service = container.service()
165+
166+
Objects assembling is consolidated in the container. When you need to make a change you do it in
167+
one place.
168+
169+
When doing a testing you call the ``container.api_client.override()`` to replace the real API
170+
client with a mock:
171+
172+
.. code-block:: python
173+
174+
from unittest import mock
175+
176+
177+
with container.api_client.override(mock.Mock()):
178+
service = container.service()
179+
180+
You can override any provider by another provider.
181+
182+
It also helps you in configuring project for the different environments: replace an API client
183+
with a stub on the dev or stage.
184+
185+
Testing, Monkey-patching and dependency injection
186+
-------------------------------------------------
187+
188+
The testability benefit is opposed to a monkey-patching.
189+
190+
In Python you can monkey-patch
191+
anything, anytime. The problem with a monkey-patching is that it's too fragile. The reason is that
192+
when you monkey-patch you do something that wasn't intended to be done. You monkey-patch the
193+
implementation details. When implementation changes the monkey-patching is broken.
194+
195+
With a dependency injection you patch the interface, not an implementation. This is a way more
196+
stable approach.
197+
198+
Also monkey-patching is a way too dirty to be used outside of the testing code for
199+
reconfiguring the project for the different environments.
200+
201+
Conclusion
202+
---------
203+
204+
Dependency injection brings you 3 advantages:
205+
206+
- **Flexibility**. The components are loosely coupled. You can easily extend or change a
207+
functionality of the system by combining the components different way. You even can do it on
208+
the fly.
209+
- **Testability**. Testing is easy because you can easily inject mocks instead of real objects
210+
that use API or database, etc.
211+
- **Clearness and maintainability**. Dependency injection helps you reveal the dependencies.
212+
Implicit becomes explicit. And "Explicit is better than implicit" (PEP20 - The Zen of Python).
213+
You have all the components and dependencies defined explicitly in the container. This
214+
provides an overview and control on the application structure. It is easy to understand and
215+
change it.
216+
217+
Is it worth to use a dependency injection in Python?
218+
219+
It depends on what you build. The advantages above are not too important if you use Python as a
220+
scripting language. The picture is different when you use Python to create an application. The
221+
larger the application the more significant is the benefit.
222+
223+
Is it worth to use a framework for the dependency injection?
224+
225+
The complexity of the dependency injection pattern implementation in Python is
226+
lower than in the other languages but it's still in place. It doesn't mean you have to use a
227+
framework but using a framework is beneficial because the framework is:
228+
229+
- Already implemented
230+
- Tested on all platforms and versions of Python
231+
- Documented
232+
- Supported
233+
- Known to the other engineers
234+
235+
Few advices at last:
236+
237+
- **Give it a try**. Dependency injection is counter-intuitive. Our nature is that
238+
when we need something the first thought that comes to our mind is to go and get it. Dependency
239+
injection is just like "Wait, I need to state a need instead of getting something right now".
240+
It's like a little investment that will pay-off later. The advice is to just give it a try for
241+
two weeks. This time will be enough for getting your own impression. If you don't like it you
242+
won't lose too much.
243+
- **Common sense first**. Use a common sense when apply dependency injection. It is a good
244+
principle, but not a silver bullet. If you do it too much you will reveal too much of the
245+
implementation details. Experience comes with practice and time.
128246

129247
What's next?
130-
~~~~~~~~~~~~
248+
------------
131249

132250
Choose one of the following as a next step:
133251

134-
- Look at application examples:
252+
- Look at the application examples:
135253
- :ref:`application-single-container`
136254
- :ref:`application-multiple-containers`
137255
- :ref:`decoupled-packages`
@@ -140,11 +258,12 @@ Choose one of the following as a next step:
140258
- :ref:`aiohttp-tutorial`
141259
- :ref:`asyncio-daemon-tutorial`
142260
- :ref:`cli-tutorial`
261+
- Know more about the ``Dependency Injector`` :ref:`key-features`
143262
- Know more about the :ref:`providers`
144263
- Go to the :ref:`contents`
145264

146265
Useful links
147-
~~~~~~~~~~~~
266+
------------
148267

149268
There are some useful links related to dependency injection design pattern
150269
that could be used for further reading:

0 commit comments

Comments
 (0)