11Dependency 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
129247What's next?
130- ~~~~~~~~~~~~
248+ ------------
131249
132250Choose 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
146265Useful links
147- ~~~~~~~~~~~~
266+ ------------
148267
149268There are some useful links related to dependency injection design pattern
150269that could be used for further reading:
0 commit comments