Virtual Subclassing

Python Virtual subclassing

Unlike some other languages, python supports the concept of virtual subclassing. What exactly is virtual subclassing? To understand the concept let’s go back to pythons roots. I am sure you are familiar with the concept of duck typing and perhaps if you have came from another language such as java, you will be familiar with the concept of an interface. In python we are not overly caught up on the type of X, but more so how X behaves, in the simplest form if it walks like a duck and quacks like a duck, chances are its a duck. Python does not support an official interface argument, however more recently Protocols can be @runtime_checkable to aid with catching issues early.

To understand why we may need virtual subclassing, let’s take a simple example. Firstly we are tasked with developing an airplane tycoon game. Our game consists of multiple things that can fly, to use the age old cliche:

  • A Bird

  • An Aeroplane

Turns out, you cannot load people into A Bird and fly them to a new destination, so we develop a few abstract base classes to easily distinguish them

from abc import ABC
from abc import abstractmethod

class Plane(ABC):
    @abstractmethod
    def fly(self):
        ...

# We make a simple plane

class MyPlane(Plane):
    def fly(self):
        print("Plane preparing to fly!")


# Now we can check in our library function
# Note: isinstance, isinstance checks against an ABC are even questionable..
def generate_flight(plane: Plane):
    if not isinstance(plane, Plane):
        raise TypeError("Not a Plane!")
    plane.fly()

# Now, if we accidentally receive a Bird, we will handle the case
# This will allow us to handle it gracefully rather than potentially
# Blow up with some other weird errors later on in the program
class Bird:
    def fly():
        print("bird flying!")

generate_flight(Bird())
"""
    TypeError                                 Traceback (most recent call last)
    <ipython-input-16-be9dc02ec41e> in <module>
    ----> 1 generate_flight(Bird())

    <ipython-input-15-d59f577c3fde> in generate_flight(plane)
         18 def generate_flight(plane: MyPlane):
         19     if not isinstance(plane, MyPlane):
    ---> 20         raise TypeError("Not a Plane!")
         21     plane.fly()
         22

    TypeError: Not a Plane!
"""
generate_flight(MyPlane())
# Plane preparing to fly!

Excellent, but whats the point in virtual subclassing still? Let’s say in the near future some other fantastic library comes along with a dozen of cool new planes, the problem is our generate_flight() function here is strictly prohibiting non explicit subclasses of Plane. Let’s take a look at the library code

# snippet from another library, not developed by you
# boeing.py
class Boeing747:
    def fly(self):
        print("Boeing 747 preparing for travel!")

    def repair(self):
        ...

Pretty simple huh, a nice shiny new Boeing that we could use in our system, except of course it does not adhere to our explicit abstract base class:

from boeing import Boeing747

generate_flight(Boeing747())
"""
TypeError                                 Traceback (most recent call last)
<ipython-input-23-24437f4c4918> in <module>
----> 1 generate_flight(Boeing747())

<ipython-input-20-8038f0273ec8> in generate_flight(plane)
     18 def generate_flight(plane: MyPlane):
     19     if not isinstance(plane, MyPlane):
---> 20         raise TypeError("Not a Plane!")
     21     plane.fly()
     22

TypeError: Not a Plane!
"""

Damn, we have coupled our library a little too tight and we don’t own the library code, what gives? Rather than monkey patching and hacking around the inheritance of Boeing747, enter virtual subclassing. We can simple register the third party code as a virtual subclass of our _interface_ (abstract base class) and the python interpreter will treat it like it has actually subclassed it.

from boeing import Boeing747

Plane.register(Boeing747)
isinstance(Boeing747(), Plane)  # True!
issubclass(Boeing747, Plane)  # Also True!
generate_flight(Boeing747())
# Boeing 747 preparing for travel!

Voila, we have successfully used third party code and ackknowledged explicitly that we accept it is an adaquate implementation of our interface.

Note: virtual subclassing should be used extremely sparingly, in reality the need for it is often miniscule. It is also possible to automatically consider objects as instance/subclasses based on their interface and python does this internally a lot in its collections.abc module, more on that in a separate post alter.