Python

How Dependency Injection Simplifies Python Code

Learn how dependency injection makes Python code more testable, maintainable, and easier to change. See practical examples of passing dependencies instead of creating them inside classes.

August 2026 4 min read 10 views 0 hearts

There's this habit I see often in Python codebases, where classes directly create the things they need. A UserService that calls Database(), a ReportGenerator that starts up Printer(). It seems harmless at first, but those little connections eventually become the reason you're afraid to change anything. One day you need to swap out a database for a different one, and suddenly you're hunting through your entire codebase to change every class that calls Database() directly.

Dependency injection is the practice of handing those dependencies to your classes from the outside, rather than letting them create dependencies on their own. Think of it not as a pattern or a framework, but more like a way of keeping your code honest.

The Classic Pain

Here's a typical Python class you might see in many projects:

class OrderProcessor:
    def __init__(self):
        self.logger = Logger()
        self.payment = PaymentGateway()
        self.email = EmailService()

At first glance, this looks fine. But what happens when you want to test OrderProcessor without sending real emails? What if the payment gateway goes down during tests? You are stuck because OrderProcessor insists on creating these real objects.

That's when testing becomes painful, and making changes becomes risky.

Passing Dependencies Instead

Dependency injection changes the approach slightly. Instead of having the class grab what it needs, you pass those things in:

class OrderProcessor:
    def __init__(self, logger, payment_gateway, email_service):
        self.logger = logger
        self.payment = payment_gateway
        self.email = email_service

That small change makes a world of difference. Now you can swap in a fake email service for testing, or a different payment gateway when the business decides to switch. The class doesn't care who provides these things, it just uses them.

When It Actually Helps

The benefit becomes clear when something changes. Imagine your payment provider increases their fees, and your company shifts to a new one. With dependency injection, you change a single configuration line somewhere in your app, and suddenly all classes that need payments get the new gateway. Without it, you are editing constructor after constructor.

At PythonSkillset, we've seen teams spend entire sprints untangling code that could have been loosely connected from the start. Dependency injection also encourages you to write smaller, focused classes, because when each dependency is passed separately, you naturally think about what a class actually needs.

A Real Example

Let's say you're building an inventory alert system:

class InventoryAlert:
    def __init__(self, notifier, data_source):
        self.notifier = notifier
        self.data_source = data_source

    def check_stock(self, product_id):
        stock = self.data_source.get_stock(product_id)
        if stock < 10:
            self.notifier.send_alert(f"Low stock for product {product_id}")

When you need to test this, you can pass a mock data source that returns specific values, and a mock notifier that just stores the messages it receives. You don't need a database running, and you don't need an actual notification system. The test becomes fast and reliable.

Should You Use a Framework?

There are frameworks like dependency_injector and inject that handle wiring for you. For small projects, they can feel like overkill. Just passing dependencies in the constructor often works fine. For larger codebases, a framework helps manage the complexity of what connects to what.

PythonSkillset's engineering team usually starts without a framework and only introduces one when the configuration becomes unwieldy. That usually happens when you have twenty or more services that need to be wired together.

What About Defaults?

Sometimes you want a default implementation but still allow overrides. That's fine:

class OrderProcessor:
    def __init__(self, logger=None):
        self.logger = logger or Logger()

This gives you the flexibility while still providing a sensible default. Just be careful not to fall back into the habit of creating complex dependencies this way, it can hide the fact that your class has too many responsibilities.

The Real Takeaway

Dependency injection isn't about following some design pattern book. It's about making your code less fragile. When each class explicitly states what it needs, and someone else provides those needs, you get code that's easier to test, easier to change, and frankly, easier to understand six months later when you come back to fix a bug.

Start with the constructors in your next project. If you're not passing dependencies in, you might want to ask yourself why.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.