
Dependency injection means a class gets the things it needs, such as a database or an email sender, handed to it from outside instead of building them itself. Usually they're passed into its constructor. Then a test can hand it a fake email sender, and the app can swap in a different one later, without changing the class.
In this post we start with a phone and its charger. Then we look at what goes wrong when a class builds its own email sender, and fix it with constructor injection, the most common kind of dependency injection. We write a small sign-up service in plain Python, run it with a real mailer, and test it with a fake one that records every email. Then we meet inversion of control, see how Spring, NestJS, Angular and FastAPI do the wiring for you, and finish with when all of this is more than you need.
A few words first. A class is like a recipe, and an object is one thing made from it, like one cake baked from that recipe. A constructor is the code that runs when an object is made; in Python it's the __init__ method. A dependency is something a class needs to do its job but isn't its job: a sign-up service needs to send emails, but sending emails is the mailer's job. A test is a small program that runs your code and checks it gives the right answer.
Key takeaways
- Dependency injection (DI) means pass in what a class needs instead of making it inside. Nothing more magical than that.
- The most common kind is constructor injection: the needs are the constructor's parameters, so one look at
__init__tells you what the class depends on. - A class that builds its own database or email sender can't be tested without the real thing, and can't be switched to another one without editing the class.
- With DI, a test passes in a fake: a stand-in that sends nothing and writes down what it was asked to do. The test then checks that list.
- Inversion of control is the bigger idea behind it: something outside your class decides what it gets and calls your code. A DI container is a framework part that does the wiring for you, as in Spring, NestJS, Angular and FastAPI.
- For a small script, pass things in by hand. Don't add a container, and don't hide your needs in a global "grab-bag" that any code can reach into.
What is dependency injection in simple words?
Think of a phone and its charger. The phone doesn't build its own power station. It has a socket, and it takes whatever charger is plugged in, as long as the plug fits the socket. At home it's the wall charger. In the car it's the car charger. At a repair shop, a technician plugs in a test meter that measures exactly how much power the phone draws. The phone itself never changes.
Now imagine a phone with a small power station built inside it, welded shut. It works. But you can never charge it any other way, and the repair shop can't measure anything without opening it up.
Code is the same. Our class is the phone. The email sender is the charger. The socket is the shape the class expects: "something with a send(to, subject, body) method". The real mailer fits that socket, and so does a fake one for tests. Dependency injection is the habit of giving every class a socket instead of a welded-in power station.
The Angular docs say it in one line: "Dependency Injection (DI) is a design pattern you use to organize and share code across your application by supplying dependencies to a class instead of creating them inside it." And they define the word dependency the same way we did: "A dependency is any object, value, function, or service that a class requires to work but does not create itself."
What problem does dependency injection solve?
Here is a sign-up service written the way most of us write our first one. When someone signs up, it remembers their email address and sends a welcome email. First, the mailer. In a real app, its send method would use Python's smtplib, which "defines an SMTP client session object that can be used to send mail to any internet machine with an SMTP or ESMTP listener daemon." In plain words: it can send mail through any mail server. (SMTP is the protocol mail servers use to pass email along, and a daemon is a program that waits in the background for work.) Ours prints the email instead, so the example runs anywhere:
# mailers.py
class ConsoleMailer:
"""Our stand-in for a real mailer: it prints the email instead of sending it."""
def send(self, to, subject, body):
print(f"To: {to} | {subject} | {body}")
And the sign-up service, with the mailer hard-wired, meaning built inside the class:
# signup_hardwired.py
from mailers import ConsoleMailer
class SignupService:
def __init__(self):
self.users = []
self.mailer = ConsoleMailer() # builds its own mailer
def sign_up(self, email):
if email in self.users:
raise ValueError(f"{email} is already signed up")
self.users.append(email)
self.mailer.send(email, "Welcome to letsBug", "Thanks for signing up!")
It works. Three things go wrong as the app grows:
- You can't test it without the real mailer. Swap our printing mailer for a real SMTP one and every test run sends a real email to a real address. Tests get slow, need a network and a mail server, and fill someone's inbox.
- You can't swap it. Moving from one email company to another means opening
SignupServiceand editing it, even though nothing about signing up changed. The same goes for a "demo mode" that sends nothing. - Its needs are hidden.
SignupService()takes no arguments, so it looks like it needs nothing. You only find out it sends email by reading every line.
Martin Fowler, the software author whose 2004 article gave this pattern its name, describes the same trap in his example, where a movie lister class makes its own movie finder: "In my naive example the lister looked up the finder implementation by directly instantiating it. This stops the finder from being a plugin." Instantiating means making an object from a class. A plugin is a part you can plug in and swap, like the charger.
Python does give you an escape hatch: unittest.mock.patch can swap ConsoleMailer for a fake while a test runs. But your test then has to know where the class looks the name up. The Python docs: "The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined." Your test is now tied to the inside of the class. Dependency injection removes the need.
How does dependency injection work?
The fix is one small change: the class stops making its mailer and asks for one instead. Somebody outside the class makes the mailer and hands it in.
Fowler's 2004 article "Inversion of Control Containers and the Dependency Injection pattern" is where the name comes from. He calls the "somebody outside" an assembler: "The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the lister class with an appropriate implementation for the finder interface". A field is a value stored on an object, like self.mailer. An interface is the shape a part must have, the socket: here, a send method.
He names three ways to hand the part in: "There are three main styles of dependency injection. The names I'm using for them are Constructor Injection, Setter Injection, and Interface Injection."
- Constructor injection: the part is a parameter of the constructor.
SignupService(mailer). - Setter injection: you make the object first, then call a method to set the part.
service.set_mailer(mailer). - Interface injection: the class implements a special method that a framework calls to hand the part in. It's rare today: Spring, for example, lists only the first two.
Fowler's advice is to start with the first one: "I would suggest you follow constructor injection unless you run into one of the specific problems with that approach, in which case switch to setter injection." His reason: "Constructors with parameters give you a clear statement of what it means to create a valid object in an obvious place." In other words, you can't make a SignupService without a mailer, so you can never forget to give it one.

How do you write dependency injection in Python?
Here's the same sign-up service with the mailer injected. It's plain Python, standard library only (the tools that come with Python), so there's nothing to install. Only three things change from the hard-wired version: the import is gone, __init__ takes a mailer, and it keeps the one it's given instead of making one.
# signup.py
class SignupService:
def __init__(self, mailer):
self.users = []
self.mailer = mailer # takes whatever mailer it is given
def sign_up(self, email):
if email in self.users:
raise ValueError(f"{email} is already signed up")
self.users.append(email)
self.mailer.send(email, "Welcome to letsBug", "Thanks for signing up!")
Notice that signup.py doesn't import any mailer. It doesn't know ConsoleMailer exists. It only knows the socket: whatever it's given must have a send(to, subject, body) method. Python doesn't check the type ahead of time; any object with that method fits. That's sometimes called duck typing: if it has the method you need, you can use it.
Somebody still has to make the real mailer. That's the job of main.py, the program's starting point, and the only file that chooses:
# main.py
from mailers import ConsoleMailer
from signup import SignupService
service = SignupService(ConsoleMailer()) # the one place that chooses
service.sign_up("asha@example.com")
Put the three files in one folder and run python main.py:
To: asha@example.com | Welcome to letsBug | Thanks for signing up!
Same output as the hard-wired version would print. The difference is who decided. When you move to a real email company, you write a new mailer class with a send method and change one line in main.py. signup.py doesn't change at all. main.py is Fowler's assembler.
How does dependency injection make testing easier?
Now the payoff. In a test, we don't want to send anything. We want to know: "did the service ask for a welcome email, to the right address?" So we plug in a fake: a small class with the same send method that sends nothing and writes down every call in a list.
# test_signup.py
import unittest
from signup import SignupService
class FakeMailer:
"""Sends nothing. Writes down every email it was asked to send."""
def __init__(self):
self.sent = []
def send(self, to, subject, body):
self.sent.append((to, subject))
class SignupTest(unittest.TestCase):
def test_sends_a_welcome_email(self):
mailer = FakeMailer()
SignupService(mailer).sign_up("asha@example.com")
self.assertEqual(mailer.sent, [("asha@example.com", "Welcome to letsBug")])
def test_no_second_email_for_the_same_address(self):
mailer = FakeMailer()
service = SignupService(mailer)
service.sign_up("ravi@example.com")
with self.assertRaises(ValueError):
service.sign_up("ravi@example.com")
self.assertEqual(len(mailer.sent), 1)
unittestis Python's built-in test tool. Each method whose name starts withtestis one test.assertEqualfails the test if the two values differ. Here it checks the fake's list: exactly one email, to Asha, with the welcome subject.- The second test signs Ravi up twice.
assertRaises(ValueError)checks that the second try is refused, and the last line checks he got only one email, not two.
Run python -m unittest in the same folder. The docs: "python -m unittest is the equivalent of python -m unittest discover." Discover means it finds every file whose name matches test*.py and runs its tests:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Each dot is one test that passed. No email was sent, no network was touched, and the tests check exactly what we care about. (The time on your machine may say 0.001s.)
This is the reason most people give for DI. The Spring docs: "As a result, your classes become easier to test, particularly when the dependencies are on interfaces or abstract base classes, which allow for stub or mock implementations to be used in unit tests." The Angular docs: "DI allows unit tests to use test doubles in place of real implementations when needed." A test double is the general name for any stand-in like ours. Fowler's "TestDouble" note lists the kinds: a stub gives fixed answers, and "Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent." So, strictly, our FakeMailer is a spy. Most people just call it a fake, and that's fine.
Fowler puts the rule of thumb bluntly: "if you can't easily stub services for testing, then this implies a serious problem with your design."
What is inversion of control?
Look at who is in charge. In the hard-wired version, SignupService decides which mailer it uses. In the injected version, it doesn't: something outside decides and hands it in. Control over that choice has been flipped, or inverted.
Inversion of control (IoC) is the wider name for this flip, and it's much older than DI. Fowler writes: "When I first ran into inversion of control, it was in the main control of a user interface." His short article on it compares two programs that ask a user questions. On the command line, your code asks each question and waits. In a window, you hand control to the windowing system, and it calls your code when the user fills in a box. "The control is inverted - it calls me rather me calling the framework. This phenomenon is Inversion of Control (also known as the Hollywood Principle - "Don't call us, we'll call you")." He adds: "Inversion of Control is a key part of what makes a framework different to a library." You call a library. A framework calls you.
Dependency injection is one kind of IoC: the control that's inverted is "who makes my parts". Fowler and other developers picked the narrower name on purpose, because the wider one was confusing people: "Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection." The Spring docs explain the old name the same way: the object doesn't make or look up its own parts, and "This process is fundamentally the inverse (hence the name, Inversion of Control)".
What is a dependency injection container?
In our example, main.py made one mailer and one service. A real app might have a hundred classes, each needing three others: a database connection, a mailer, a payment client, a logger. Writing all that wiring by hand, in the right order, gets long. A DI container (also called an IoC container) is the part of a framework that does it for you. You tell it which classes exist and what each one needs, and it builds them, parts first, and hands each one in.

main.py does, for every class in the app. Each framework has its own way for a class to ask for what it needs.Four you'll meet, briefly:
- Spring (Java). Objects the container manages are called beans: "A bean is an object that is instantiated, assembled, and managed by a Spring IoC container." A bean lists its needs as constructor parameters (or setters), and "The IoC container then injects those dependencies when it creates the bean." Spring's docs name the same two styles Fowler recommends: "DI exists in two major variants: Constructor-based dependency injection and Setter-based dependency injection."
- NestJS (Node.js, TypeScript). "Nest is built around the dependency injection design pattern." A controller asks for a service in its constructor, like
constructor(private catsService: CatsService) {}, and "Nest resolves dependencies by their type." By default it makes one shared copy: "With the default (singleton) scope, Nest creates the instance once and shares it with every class that depends on it." A singleton is a class with only one object in the whole app. - Angular (TypeScript, in the browser). Components and services ask for what they need: "You can inject dependencies using Angular's inject() function." For example,
private analytics = inject(AnalyticsLogger);inside a component. - FastAPI (Python). A route function marks a parameter with
Depends(get_mailer), whereget_maileris a function that makes the part. FastAPI's docs: "Whenever a new request arrives, FastAPI will take care of:" and first on the list, "Calling your dependency ("dependable") function with the correct parameters." For tests, you swap a dependency withapp.dependency_overrides; the docs say "it is a simple dict." Their example is an outside login service that "might be charging you per request, and calling it might take some extra time than if you had a fixed mock user for tests."
Different syntax, same idea as our 11-line signup.py: the class says what it needs, and something else provides it.
Dependency injection vs service locator: what's the difference?
There's a second way to get parts into a class without building them inside, and Fowler's article compares the two. A service locator is a central object that a class asks for its parts while it runs: "The basic idea behind a service locator is to have an object that knows how to get hold of all of the services that an application might need." Here it is in its simplest form, a shared dictionary:
# signup_locator.py (a service locator, in its simplest form)
from mailers import ConsoleMailer
SERVICES = {"mailer": ConsoleMailer()} # any code can read or swap these
class SignupService:
def sign_up(self, email):
SERVICES["mailer"].send(email, "Welcome to letsBug", "Thanks for signing up!")
The difference is the direction. Fowler: "With service locator the application class asks for it explicitly by a message to the locator. With injection there is no explicit request, the service appears in the application class - hence the inversion of control." You can still swap the mailer in a test, by changing SERVICES["mailer"].
Fowler doesn't call either one wrong. "When building application classes the two are roughly equivalent, but I think Service Locator has a slight edge due to its more straightforward behavior. However if you are building classes to be used in multiple applications then Dependency Injection is a better choice." The inversion that injection brings is the part he's wary of: "So on the whole I prefer to avoid it unless I need it."
This post uses constructor injection for two reasons. First, the needs are visible. Look at SignupService() in the locator version: no parameters, so it looks like it needs nothing. Fowler: "With dependency injector you can just look at the injection mechanism, such as the constructor, and see the dependencies. With the service locator you have to search the source code for calls to the locator." Second, nothing is shared. SERVICES is global (any code anywhere can read and change it), so a test that swaps the mailer and forgets to put it back leaves the fake in place for every test that runs after it. A constructor-injected fake disappears with its test.
His bigger point is the one to remember: "The choice between Service Locator and Dependency Injection is less important than the principle of separating service configuration from the use of services within an application." Choose the parts in one place; use them everywhere else.
When is dependency injection overkill?
DI is a tool, and Fowler is careful about the cost of handing control to a framework or container: "Inversion of control is a common feature of frameworks, but it's something that comes at a price. It tends to be hard to understand and leads to problems when you are trying to debug." Some honest cases where you don't need it:
- A short script. A 50-line script that renames your photos, run once, doesn't need sockets. Build what you need where you need it.
- Parts that never change and touch nothing outside. Don't inject Python's
mathmodule, a list, or a small helper function that only does arithmetic. Inject the parts that talk to the outside world (a database, email, a payment service, the network, the clock, random numbers) or that you really want to swap. Those are the ones that make tests slow or flaky. - A container in a small app. Passing parts in by hand, in
main.py, is still dependency injection. It's the same pattern without the framework. Reach for a container when the hand wiring gets long, or when your framework (Spring, NestJS, Angular, FastAPI) already has one built in.
What mistakes do beginners make with dependency injection?
1. A container for a 50-line script. Installing a DI library, writing a config file and registering three classes, for a program that main.py could wire in two lines. Now there's more wiring code than real code, and a newcomer has to learn the container before they can read the program. Start by hand. Add a container when the wiring hurts.
2. A global grab-bag with no plan for tests. The careless version of the service locator above: a global dictionary that every class quietly reaches into, filled with real parts and never meant to be swapped. Tests then either send real emails, or swap a part and forget to put it back, so the fake leaks into every test after it. Fowler suspects that people who find locators hard to test come from "projects where people don't make the effort to ensure that their service locator can be easily substituted." If you use a locator, make swapping and resetting it part of your tests from day one.
3. Injecting everything. Twelve constructor parameters, including a list and a number formatter, because "DI is good". Inject the parts that talk to the outside world or that you want to swap. Build the rest in place.
1) With FakeMailer, sign up "asha@example.com" and then "ravi@example.com" on the same SignupService. What's in mailer.sent?
2) Your app needs a demo mode that sends no email at all. Write a mailer for it. How many lines of signup.py do you change?
3) Without changing signup_hardwired.py, how could a test check that it tried to send Asha a welcome email? Why is that worse than the injected version?
Show the answers
1) Two entries, in order: [("asha@example.com", "Welcome to letsBug"), ("ravi@example.com", "Welcome to letsBug")]. The fake only keeps the address and the subject.
2) Zero lines. Write a new mailer with the same socket, and plug it in from main.py with SignupService(NullMailer()):
class NullMailer:
"""Demo mode: sends nothing at all."""
def send(self, to, subject, body):
pass
3) Our hard-wired mailer prints, so a test can catch what it prints with contextlib.redirect_stdout, a "Context manager for temporarily redirecting sys.stdout to another file object." (sys.stdout is where print writes.)
import io
from contextlib import redirect_stdout
from signup_hardwired import SignupService
out = io.StringIO()
with redirect_stdout(out):
SignupService().sign_up("asha@example.com")
assert "To: asha@example.com | Welcome to letsBug" in out.getvalue()
It works, but only because our mailer prints. With a real SMTP mailer inside, the same test would send a real email, and capturing the output would tell you nothing. You'd be back to unittest.mock.patch and knowing where the class looks its mailer up. The injected version needs none of that.
Questions people ask
What is dependency injection in simple words?
Giving a class the things it needs from outside, instead of letting it build them itself. A sign-up service that needs to send email gets a mailer passed into its constructor. In the app you pass the real mailer; in tests you pass a fake that just records what it was asked to send.
What is the difference between dependency injection and inversion of control?
Inversion of control is the wider idea: something outside your code is in charge and calls your code, the way a framework calls your handlers. Dependency injection is one kind of it, where what's inverted is who makes a class's parts. Martin Fowler and other developers settled on the name in 2004 because "inversion of control" was too broad a term.
What are the types of dependency injection?
Fowler names three: constructor injection (parts are constructor parameters), setter injection (parts are set by a method after the object is made) and interface injection (the class implements a method a framework calls to hand parts in). He suggests constructor injection unless you hit a specific problem with it, then setter injection.
Do I need a framework or container to use dependency injection?
No. Passing parts into a constructor by hand, in one place such as main.py, is dependency injection. A container only automates that wiring. It's useful in big apps, and frameworks like Spring, NestJS, Angular and FastAPI have one built in, but a small program doesn't need one.
What is the difference between dependency injection and a service locator?
With dependency injection, a class is handed its parts and never asks for them. With a service locator, the class asks a central object for its parts while it runs. Both let you swap parts for tests. With DI the constructor shows what a class needs; with a locator you have to search the code for calls to it. Fowler sees both as fair choices inside one app, and prefers DI for classes used in many apps.
Keep going
- Previous in this System Design series: Monolith vs microservices vs modular monolith, where modules talk only through public doors. DI is how each module gets the parts behind those doors.
- Next: "Repository pattern", or how to give a class its database the same way we gave it a mailer.
- Where the series began: MVC architecture explained. A controller is a common place to inject services.
Checked on 28 October 2026 against the 13 sources below.
Sources
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern (2004), the naming, the three styles, service locator
- Martin Fowler: InversionOfControl (2005)
- Martin Fowler: TestDouble (2006), the kinds of test double
- Spring Framework reference: Introduction to the Spring IoC Container and Beans
- Spring Framework reference: Dependency Injection
- NestJS documentation: Providers (dependency injection)
- Angular documentation: Dependency injection in Angular
- FastAPI documentation: Dependencies
- FastAPI documentation: Testing Dependencies with Overrides
- Python documentation: smtplib, SMTP protocol client
- Python documentation: unittest, Unit testing framework (test discovery)
- Python documentation: unittest.mock (where to patch)
- Python documentation: contextlib (
redirect_stdout)
Every Python sample in this post was run in Python 3.13 before publishing, and the tests were run against the fake mailer.