Clean Architecture in Python

Hidden Power of Polymorphism in Python

How to implement polymorphic interfaces within application.

Hidden Power of Polymorphism in Python

Polymorphism is often explained as part of the OOP paradigm. But Python has other powerful features to implement abstractions than object-oriented design. There are a few main ways to create polymorphic interfaces within an application. And choosing the proper way becomes important when we want to implement the clean and powerful architecture.

Theory

Polymorphism and Type System are the main features of a language that allows us to communicate ideas into a code. Actually, polymorphism helps us to make code reusable, testable, and maintainable.

Let’s start with some formal definition from Wikipedia.

Polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types.

The main kinds of polymorphism are:

Ad hoc variant goes to the languages with a possibility to overload functions, C for example. Parametric is for the languages that have generics and type inference (C++, Haskell). We can use row polymorphism in Typescript. Python has the two of these types. They are Subtyping and Duck typing.

Showcase

For example, let come up with a simple showcase of some storage. This storage can add an item and get it by its primary key. This case is a business case, and we do not know what storage we have. It can be a database or even another service. We do not worry about these details.

def use_storage(storage):
    storage.add({"pk": 1, "data": "text"})
    storage.get(1)

Subtyping/subclassing

Classes and OOP design often appear when we start talking about patterns and architecture of code. Inheritance allows new objects to be defined from existing ones. And by defining the hierarchy of classes, we can model complex relationships between objects in our domain.

Lets define abstract class Storage that has an interface to add and to get items.

class Storage:
    def add(self, item: dict):
        raise NotImplementedError

    def get(self, pk: int):
        raise NotImplementedError

When we define class, along with that, we introduce a new type in our code. We can add type hinting to the function use_storage(storage: Storage). And use type checkers, for instance, mypy or pyre.

Then we define two specific classes Memory and Persistent which are subclassed from our abstract Storage.

class Memory(Storage):
    def add(self, item: dict):
        print(f"[memory] put {item}")

    def get(self, pk: int):
        print(f"[memory] get item with {pk=}")


class Persistent(Storage):
    def add(self, item: dict):
        print(f"[persistent] put {item}")

    def get(self, pk: int):
        print(f"[persistent] get item with {pk=}")

These two classes inherit methods and properties of their parent class Storage. We can use them anywhere the Storage is expected and pass the instances of these classes to our use_storage function.

What are the pros and cons of defining abstract Storage and introduce hierarchy of classes?

Pros:

Cons:

Abstractions have two sides, on one hand, they help to hide complexity, from another, they add complexity. And it is important to keep balance.

In this case, I would say that it is “You Aren’t Gonna Need It” (YAGNI) than about real benefits. What if we remove the abstract class and will go only with the specific implementations? That leads us to the duck typing.

Duck typing with classes

As being a highly dynamic language, some of Python’s principles exist in the form of agreement. Encapsulation of the private attributes in classes is a classic example. There is an agreement that methods or attributes started with an underscore(_) are private. Also, we can remember import this that defines the philosophy of the whole language. We may say that duck typing is also kind of agreement.

If it acts like a duck, then it must be a duck.

Duck typing in Python is the row polymorphism not related to a type. In this case, the object’s behavior is determined by its structure. This means that we can provide any entity that contains the methods or the attributes we expect.

Practically, when we define a class we define an interface. In this sense, that we define how we can interact with an instance of that class.

In our example, we get rid of abstract class and inheritance and leave only our specific classes.

class Memory:
    def add(self, item: dict):
        print(f"put {item} to the storage")

    def get(self, pk: int):
        print(f"get item with {pk=}")


class Persistent:
    def add(self, item: dict):
        print(f"put {item} to the storage")

    def get(self, pk: int):
        print(f"get item with {pk=}")

These implementations still follow the same interface and can be used in the same use_storage function.

Pros:

Cons:

Classes are a great tool to model real-world behavior in our code. But sometimes, we don’t have an internal state that should be encapsulated and/or changed, or some entity will exist as a single instance. There are cases when classes become over-engineering.

We can go further and remove classes.

Duck typing with modules

An entity is not required to be a class to be polymorphic. Modules can also follow the rule of duck typing. We can define interfaces on the higher level by using modules, their hierarchy, and imports.

Let’s move forward by defining a directory structure for our example.

storage/
  __init__.py
  memory.py
  persistent.py

Then move out implementations respectively.

# file storage/__init__.py
from . import memory, persistent  # noqa


# file storage/memory.py
def add(item: dict):
    print(f"[memory] put {item}")

def get(pk: int):
    print(f"[memory] get item with {pk=}")


# file storage/persistent.py
def add(item: dict):
    print(f"[persistent] put {item}")

def get(pk: int):
    print(f"[persistent] get item with {pk=}")

And we can use it in the same function use_storage.

import storage.memory
import storage.persistent
use_storage(storage.memory)
# or
use_storage(storage.persistent)

These objects have all the advantages.

Pros:

Cons:

One more advantage is that you can have an expressive and well-named structure in application. Defining high-level code flow by the modules and use them explicitly in a code. I think this is what Robert Martin called Screaming Architecture.

So what does the architecture of your application scream? When you look at the top-level directory structure, and the source files in the highest-level package, do they scream “Health Care System,” or “Accounting System,” or “Inventory Management System”? Or do they scream “Rails,” or “Spring/ Hibernate,” or “ASP”?
- Robert. C. Martin. Clean Architecture.

Final thoughts

There are plenty of programming concepts in Python, and a developer can choose a functional, object-oriented, or procedural paradigm depending on current tasks. There is no universal advice.

The same is with polymorphic interfaces, we can choose any of the ways to implement entities and their relationships in the code. Clean, expressive, and extensible are the features we want to see as the result. Abstractions can hide complexity, KISS and YAGNI can help stay clear, and remember that

Simple is better than complex.

Code examples can be found in my GitHub repo py-polymorphism-study.

More reading

If you like this article you can be interested in the following.

Thank you for reading! Share you thoughts with me on LinkedIn and Twitter.