Python Dependency Inversion Principle

In this post I’ll show what dependency inversion looks like in the context of a dynamically typed language like Python. But first, I’m going to introduce the concept of dependency inversion and what it means in a statically typed language like Java so that we can see the differences between the two types of languages.

Dependency Inversion

According to wikipedia, the dependency inversion principle states that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

Put another way, it’s saying that if a component needs to be able to switch specific implentations (details), it should not have a source code dependency to those implementations. By removing the source code dependency and replacing it with a dependency to an interface, you are “inverting” the depedency between the high level component and the low level details.

Java

In java, this is an example without dependency inversion.

class Cat {
    public void greet() {
        AngryGreeter greeter = new AngryGreeter();
        greeter.greet();
   }
}

The Cat class is our component here. It is directly referencing the class AngryGreeter which contains the specific details of greeting. In many cases, this source code dependency runs in the same direction as the flow of control. This isn’t too suprising because if Cat needs to invoke methods on instances of AngryGreeter, it needs to have a reference to a greeter object and the most common way to do that is to just use the new keyword to construct the object where it’s needed.

The consequence of this relationship through this construction is that it creates tight coupling between the two modules. More specifically, our Cat depends on AngryGreeter. If there is no need to change greeting behavior at all, this relationship is perfectly fine.

If we want to add new behaviors, we would have to modify Cat everytime.

In order to do that, we need to:

  1. Introduce an interface that concrete greeters impelement
  2. Pass concrete greeters as arguments into Cat (dependency injection)

Here’s what dependency inversion looks like in Java for this simple example:

public interface Greeter{
	void greet();
}

class AngryGreeter implements Greeter { 
    public void greet() {
        System.out.println("YOWL!");
    }
}

class Cat {
    private final Greeter greeter;
	    
    public Cat(Greeter greeter) {
        this.greeter = greeter;
    }

    public void greet() {
        this.greeter.greet();
    }
}

Now, rather than Cat depending directly on AngryGreeter, both Cat and Angry greeter depend on the interface Greeter. What we have now is a dependency that points in the opposite direction of the control flow, hence it’s inverted.

Now, we can add new greeters without ever touching the Cat component. Previously, Cat had a direct, hard coded reference to AngryGreeter. Now it has a direct reference to the interface instead which will only change when the greeting API changes (and not when new greeting behavior that uses the same API is added).

Python

Now lets look at the first example implemented in Python:

class AngryGreeter:
    def greet(self):
        print("YOWL!")
class Cat:
    def greet(self):
        greeter = AngryGreeter()
        greeter.greet()

Since python is dynamically typed, there is no need to declare an interface the source code in the same way as Java. At run time, objects either can do something or they can’t. So if we wanted to invert the dependencies, we typically just make the name of the instance something generic and make it an argument:

class AngryGreeter:
    def greet(self):
        print("YOWL!")
class Cat:
    def greet(self, greeter)
        greeter.greet()

While this is really easy to do in python (or really any dynamically typed language), there is one glaring drawback when we do this: we don’t know exactly what the interface is without looking at what methods are actually being called.

So while inverting dependencies are easy because interfaces are implicit, the implictness of interfaces means that:

  • Cat may crash if given a greeter object that does not implement the same interface. Missing method?
  • You need to spend a lot more time reading existing source code (mostly existing concrete classes) in order to infer what the interface is

Abstract Base Classes

Python 3 introduced ABC classes to solve these problems (these are more akin to javas abstract interfaces since they can contain implementation).

import abc

class Greeter(abc.ABC):
    @abc.abstractmethod
    def greet(self):
        pass

class AngryGreeter(Greeter):
    def greet(self):
        print("YOWL!")

class HappyGreeter(Greeter):
    pass

class Cat:
    def greet(self, greeter):
        greeter.greet()

if __name__ == "__main__":
    c = Cat()
    c.greet(AngryGreeter())
    c.greet(HappyGreeter())

Now, you’ll get an exception when HappyGreeter is instantiated without the greet method. In terms of understanding what methods are expected, we don’t have to go digging through multiple classes - we can just look at the interface that the greeters implement.