The Python programming language doesn’t include direct support for interfaces in the same way as other object-oriented programming languages. However, it is possible to construct the same functionality in Python with just a little bit of work. For the full context, check out Implementing in Interface in Python from Real Python. It includes a much deeper discussion of the different aspects of this code and why we use it.
Formal Python Interface
To create an interface in Python, we will create a class that includes several different elements. Let’s look at an example for a
MyCollection interface that we could create, which can be used for a wide variety of collection classes like lists, stacks, and queues:
import abc from typing import List class IMyCollection(metaclass=abc.ABCMeta): @classmethod def __subclasshook__(cls, subclass: type) -> bool: if cls is IMyCollection: attrs: List[str] = ['size', 'empty'] callables: List[str] = ['add', 'remove', 'get', 'contains'] ret: bool = True for attr in attrs: ret = ret and (hasattr(subclass, attr) and isinstance(getattr(subclass, attr), property)) for call in callables: ret = ret and (hasattr(subclass, call) and callable(getattr(subclass, call))) return ret else: return NotImplemented @property @abc.abstractmethod def size(self) -> int: raise NotImplementedError @property @abc.abstractmethod def empty(self) -> bool: raise NotImplementedError @abc.abstractmethod def add(self, o: object) -> bool: raise NotImplementedError @abc.abstractmethod def remove(self, i: int) -> bool: raise NotImplementedError @abc.abstractmethod def get(self, i: int) -> object: raise NotImplementedError @abc.abstractmethod def contains(self, o: object) -> bool: raise NotImplementedError
This code includes quite a few interesting elements. Let’s review each of them:
- First, we import the
abclibrary, which as you may recall is the library for Abstract Base Classes.
- We’re also importing the
Listclass from the
typinglibrary to assist with some type checking.
- In the class definition for our
IMyCollectionclass, we are listing the
abc.ABCMetaclass as the metaclass for this class. This allows Python to perform some analysis on the code itself. You can read more about Python Metaclasses from Real Python.
- Inside of the class, we are overriding one class method,
__subclasshook__. This method is used to determine if a given class properly implements this interface. When we use the Python
issubclassmethod, it will call this method behind the scenes. See below for a discussion of what that method does.
- Then, each property and method in the interface is implemented as an abstract method using the
@abc.abstractmethoddecorator. Those methods simply raise a
NotImplementedError, which enforces any class implementing this interface to provide implementations for each of these methods. Otherwise, the Python interpreter will raise that error for us.
__subclasshook__ method in our interface class above performs a task that is normally handled automatically for us in many other programming languages. However, since Python is dynamically typed, we will want to override this method to help us determine if any given object is compatible with this interface. This method uses a couple of metaprogramming methods in Python.
First, we must check and make sure the class that this method is being called on,
cls, is our interface class. If not, we’ll need to return
NotImplemented so Python will continue to use the normal methods for checking type.^[See https://stackoverflow.com/questions/40764347/python-subclasscheck-subclasshook for details]
Then, we see two lists of strings named
attrs list is a list of all of the Python properties that should be part of our interface - in this case it should have a
empty property. The
callables list is a list of all the callable methods other than properties. So, our
IMyCollection class will include
Below that, we find two
for loops. The first loop will check that the given class, stored in the
subclass, contains properties for each item listed in the
attrs list. It first uses the
hasattr metaprogramming method to determine that the class has an attribute with that name, and then uses the
isinstance method along with the
getattr method to make sure that attribute is an instance of a Python property.
Similarly, the second
for loop does the same process for the methods listed in the
callables list. Instead of using
isinstance, we use the
callable method to make sure that the attribute is a callable method.
This method is a little complex, but it is a good look into how the compiler or interpreter for other object-oriented languages performs the task of making sure a class properly implements an interface. For our use, we can just copy-paste this code into any interface we create, and then update the
callables lists as needed.
A Second Interface
Let’s look at one more formal Python interface, this time for a stack:
import abc from typing import List class IMyStack(metaclass=abc.ABCMeta): @classmethod def __subclasshook__(cls, subclass: type) -> bool: if cls is IMyStack: attrs: List[str] =  callables: List[str] = ['push', 'pop', 'peek'] ret: bool = True for attr in attrs: ret = ret and (hasattr(subclass, attr) and isinstance(getattr(subclass, attr), property)) for call in callables: ret = ret and (hasattr(subclass, call) and callable(getattr(subclass, call))) return ret else: return NotImplemented @abc.abstractmethod def push(self, o: object) -> None: raise NotImplementedError @abc.abstractmethod def pop(self) -> object: raise NotImplementedError @abc.abstractmethod def peek(self) -> object: raise NotImplementedError
This is a simpler interface which simply defines methods for
Once we’ve created an interface, we can then create a class that implements that interface. Any class that implements an interface must provide an implementation for all methods defined in the interface.
For example, we can create a
MyList class that implements the
IMyCollection interface defined above, as shown in this example:
from typing import List class MyList(IMyCollection): def __init__(self) -> None: self.__list: List[object] = list() self.__size: int = 0 @property def size(self) -> int: return self.__size @property def empty(self) -> bool: return self.__size == 0 def add(self, o: object) -> bool: self.__list.append(o) self.__size += 1 return True def remove(self, i: int) -> bool: del self.__list[i] return True def get(self, i: int) -> object: return self.__list[i] def contains(self, o: object) -> object: for obj in self.__list: if obj == o: return True return False
Notice that we include the interface class in parentheses as part of the class declaration, which will tell Python the interface that we are implementing in this class. Then, in the class, we include implementations for each method defined in the
IMyCollection interface. Those implementations are simple and full of bugs, but they give us a good idea of what an implementation of an interface could look like. We can also include more attributes and a constructor, as well as additional methods as needed.
Python also allows a class to implement more than one interface. This is a special type of inheritance called multiple inheritance. Any class that implements multiple interfaces must provide an implementation for every method defined in each of the interfaces it implements.
For example, we can create a special
MyListStack class that implements both the
IMyStack interfaces we defined above:
from typing import List class MyListStack(IMyCollection, IMyStack): # include all of the code from the MyList class def push(self, o: object) -> None: self.add(o) def pop(self) -> object: out = self.__list[self.__size - 1] self.remove(self.__size - 1) return out def peek(self) -> object: return self.__list[self.__size - 1]
To implement multiple interfaces, we can simply list them inside of the parentheses as part of the class definition, separated by a comma.
Interfaces as Types
Finally, recall from the previous page that we can treat any interface as a data type, so we can treat classes that implement the same interface in the same way. Here’s an example:
collects: List[IMyCollection] = list() collects.append(MyList()) collects.append(MyListStack()) collects.add("String") collects.add("Hello")
However, it is important to remember that, because the second element in the
collects array is an instance of the
MyListStack class, we can also access the
pop methods directly. This is because Python uses dynamic typing and duck typing, so as long as the object supports those methods, we can use them. Put another way, if the object is able to receive those messages, we can pass them to the object.
There are two special methods we can use to determine the type of an object in Python.
if isinstance(collects, MyListStack): # do something
isinstance method in Python is used to determine if an object is an instance of a given class.
if issubclass(collects, IMyStack): # do something
issubclass method is used to determine if an object is a subclass of a given class. Since we are creating a formal interface in Python and overriding the
__subclasshook__ method, this will determine if the object properly includes all required properties and methods defined by the interface.