Interfaces in Python

Conrad Mugabe
CodeX
Published in
4 min readApr 24, 2024

--

Photo by Jonny Gios on Unsplash

Interfaces in programming are like jigsaw puzzle pieces. They define a contract for how different parts of a system should interact, just as puzzle pieces fit together. They ensure compatibility and allow components to work together seamlessly.

We could look at a publicly available API like https://jsonplaceholder.typicode.com/ as an example. They provide documentation on how to interact with their API. A set of guidelines must be followed to work with the API. They dictate inputs and outputs to their interface. We don’t care what happens underneath, we care about the output and input.

What is an Interface

An interface is a contract that a system has with the outside world on how to interact with it (the system). A good contract shows the input and the expected output. Interfaces do not contain any logic/implementations.

Why interfaces?

Interfaces can enable or support different powerful software development paradigms such as;

  1. Abstraction

Interfaces are an absolute abstraction since they expose function signatures. Interfaces allow services to hide implementation details from consumers. The details of how a service’s functions are implemented are hidden.

2. Loose coupling

Coupling refers to the dependency of a code, service, or system on another service. Interfaces allow us to separate implementation details from function signatures.

3. Liskov Substitution Principle (LSP)

In a 1994 paper titled A Behavioral Notion of Subtyping, Barbar Liskov and Jeannette Wing write as follows:

This paper takes the position that the relationship should ensure that any property proved about supertype objects also holds for its subtype objects, and presents two ways of defining the subtype relation.

This is one of the SOLID Principles popularized by Robert C Martin in his book Agile Software Development, Principles, Patterns, and Practices.

4. Multiple Inheritance

Programming languages that do not support multiple inheritance, like Java because of the diamond problem achieve multiple inheritance by leveraging the power of interfaces. Interfaces allow languages like Java to get by the diamond problem.

5. Clean Architecture

Clean architectures such as Hexagonal Architecture rely upon abstractions and no direct references to implementation details.

Interfaces In Python

Python does not have an explicit interface keyword but if we consider an interface as a contract, we can go around this limitation. An inbuilt package abc (Abstract Base Class) exposes a class, ABC and a function, abstractmethod that we can use to implement interfaces.

A Use Case

Assume we are building an application that persists data to a storage service. A contract is agreed on the input and output of this service. In this case, a storage service will have a save method with a string parameter and return a string. We don’t care about implementation details.

"""Database service"""

from abc import ABC, abstractmethod


class DatabaseService(ABC):
"""Database Service"""

@abstractmethod
def save(self, item: str) -> str:
"""persist to storage"""

We can implement a storage service, one using MongoDB and the other using PostgreSQL. The goal is to respect the database service contract.

"""Mongo Database Service"""


class MongoDatabaseService(DatabaseService):
"""Mongo Database Service"""

def save(self, item: str) -> str:
"""persist to storage"""
# actual logic to implement service
return item
"""PostgreSQL Database Service"""


class PostgreSQlDatabaseService(DatabaseService):
"""PostgreSQL Database Service"""

def save(self, item: str) -> str:
"""persist to storage"""
# actual logic to implement service
return item

Using The Service

In the code snippet below, UseCases class has all the business rules of our application. The DatabaseService can be substituted by any database service that implements this interface.

"""Use Cases"""


class UseCases:
"""Use Cases"""

def __init__(self, database: DatabaseService) -> None:
self.database = database

def save_item(self, item: str) -> str:
"""save item"""
# some logic
saved_item = self.database.save(item)
# more logic
return saved_item

What to know about abstract classes in Python

  1. An abstract class should have at least one abstract method.
"""Database service"""

from abc import ABC, abstractmethod


class DatabaseService(ABC):
"""Database Service"""

The code above does not represent an abstract class.

2. Python does not enforce parameters and return values

"""Database service"""

from abc import ABC, abstractmethod


class DatabaseService(ABC):
"""Database Service"""

@abstractmethod
def save(self, item: str) -> str: # 👈 Notice the parameter type
"""persist to storage"""


class MongoDatabaseService(DatabaseService):
"""Mongo Database Service"""

def save(self, item: int) -> str: # 👈 Notice the parameter type
"""persist to storage"""
return item

In the code above, the save methods take an item, one has a type of string and the other integer. This is a clear violation of the Liskov Substitution Principle which expects that a child class should be substituted for its parent class. Integer and string cannot be substituted.

The workaround for this is to rely on mpypy to enforce type-checking.

Conclusion

Interfaces act as clear documentation of what a class or component should do. This makes it easier to understand how different parts of the system interact.

Interfaces allow us to test our services in isolation. This makes tests easier to write, understand, and maintain. You can mock or stub external dependencies defined by the interface, focusing on the core functionality of the code you’re testing.

--

--