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
abc
library, which as you may recall is the library for Abstract Base Classes. - We’re also importing the
List
class from thetyping
library to assist with some type checking. - In the class definition for our
IMyCollection
class, we are listing theabc.ABCMeta
class 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 Pythonissubclass
method, 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.abstractmethod
decorator. Those methods simply raise aNotImplementedError
, 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
The __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
and callables
. The 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 size
and empty
property. The callables
list is a list of all the callable methods other than properties. So, our IMyCollection
class will include add
, remove
, get
, and contains
methods.
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 attrs
and 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 push
, pop
, and peek
.
Implementing Interfaces
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.
Multiple Inheritance
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 IMyCollection
and 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[0].add("String")
collects[1].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 push
and 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[1], MyListStack):
# do something
The isinstance
method in Python is used to determine if an object is an instance of a given class.
if issubclass(collects[1], IMyStack):
# do something
The 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.
References
- Implementing an Interface in Python from Real Python
- Python Metaclasses from Real Python