Chapter 6

Inheritance & Polymorphism

Like superclass, like subclass! Now, with interfaces!

Subsections of Inheritance & Polymorphism

Introduction

Content Note

Much of the content in this chapter was adapted from Nathan Bean’s CIS 400 course at K-State, with the author’s permission. That content is licensed under a Creative Commons BY-NC-SA license.

The term polymorphism means many forms. In computer science, it refers to the ability of a single symbol (i.e. a function or class name) to represent multiple types. Some form of polymorphism can be found in nearly all programming languages.

While encapsulation of state and behavior into objects is the most central theoretical idea of object-oriented languages, polymorphism - specifically in the form of inheritance - is a close second. In this chapter we’ll look at how polymorphism is commonly implemented in object-oriented languages.

Key Terms

Some key terms to learn in this chapter are:

  • Polymorphism
  • Type
  • Type Checking
  • Casting
  • Implicit Casting
  • Explicit Casting
  • Interface
  • Inheritance
  • Superclass
  • Subclass
  • Abstract Classes

Types

YouTube Video

Video Materials

Before we can discuss polymorphism in detail, we must first understand the concept of types. In computer science, a type is a way of categorizing a variable by its storage strategy, i.e., how it is represented in the computer’s memory. It also defines how the value can be treated and what operations can be performed on it.

You’ve already used types extensively in your programming up to this point. Consider the declaration:

int number = 5;
number: int = 5

The variable number is declared to have the type int. In Java, the type included in the declaration tells the Java compiler that the value of the number will be stored using a specific scheme for integer values. For Python, the type is implied by the value itself - since 5 is a whole number, it is treated like an integer. The type annotation int is used by Mypy for type checking, but us ignored by the Python interpreter itself.

Each language stores these values in memory differently, and we won’t worry about those technical differences in this course. What is important to remember is that the variable’s data type tells the computer how to store that value, and also what operations can be performed on that value.

For example, consider the following code:

int x = 5;
int y = 7;
String string = " apples";
System.out.println(x + y); // 12
System.out.prinltn(x + string); // 5 apples
x: int = 5
y: int = 7
string: str = " apples"
print(x + y) # 12
print(x + string) # TypeError

Consider the last two lines of each example - we are using the plus + operator between two different variables. In the first case, the two operands x and y are both integers. So, the computer will know that the plus operator should be treated like addition, and it will add those two integer values together.

In the second case, one operand x is an integer, but the other operand string is a string value. What should happen in that case? As it turns out, each language does this a bit differently. In Java, the plus operator can also be used for concatenation, so the result will be 5 apples. Python, however, will raise a TypeError since it doesn’t know what the plus operator means when applied to a string and an integer.

In either case, our computer is able to use the data type assigned to each variable to determine how it should be treated and what operations it can perform.

User-Defined Types

In addition to built-in types, most programming languages support user-defined types, that is, new types defined by the programmer. For example, we could define an enumerator called Grade:

public enum Grade {
  A,
  B,
  C,
  D,
  F;
}
from enum import Enum


class Grade(Enum):
  A = 1
  B = 2
  C = 3
  D = 4
  F = 5

This defines a new data type Grade. We can then create variables with that type:

Grade courseGrade = Grade.A;
course_grade: Grade = Grade.A

Classes are Types

In an object-oriented programming language, a class also defines a new type! As we discussed in an earlier chapter, a class defines the structure for the state for objects implementing that type. Consider a class named Student as shown in this example:

public class Student {
    
    private int creditPoints;
    private int creditHours;
    private String first;
    private String last;
    
    // accessor methods for first and last omitted

    public Student(String first, String last) {
        this.first = first;
        this.last = last;
    }
    
    /**
     * Gets the student's grade point average.
     */
    public double getGPA() {
        return ((double) creditPoints) / creditHours;
    }
    
    /**
     * Records a final grade for a course taken by this student.
     * 
     * @param grade       the grade earned by the student
     * @param hours       the number of credit hours in the course
     */
    public void addCourseGrade(Grade grade, int hours) {
        this.creditHours += hours;
        switch(grade) {
            case A:
                this.creditPoints += 4 * hours;
                break;
            case B:
                this.creditPoints += 3 * hours;
                break;
            case C:
                this.creditPoints += 2 * hours;
                break;
            case D:
                this.creditPoints += 1 * hours;
                break;
            case F:
                this.creditPoints += 0 * hours;
                break;
        }
    }
}
class Student:

    def __init__(self, first: str, last: str) -> None:
        self.__first: str = first
        self.__last: str = last
        self.__credit_points: int = 0
        self.__credit_hours: int = 0
        
    # properties for first and last omitted
    
    @property
    def gpa(self) -> float:
        """Gets the student's grade point average.
        """
        return self.__credit_points / self.__credit_hours
    
    def add_course_grade(self, grade: Grade, hours: int) -> None:
        """Records a final grade for a course taken by this student.
        
        Args
           grade: the grade earned by the student
           hours: the number of credit hours in the course
        """
        self.__credit_hours += hours
        if grade == Grade.A:
            self.__credit_points += 4 * hours
        elif grade == Grade.B:
            self.__credit_points += 3 * hours
        elif grade == Grade.C:
            self.__credit_points += 2 * hours
        elif grade == Grade.D:
            self.__credit_points += 1 * hours
        elif grade == Grade.F:
            self.__credit_points += 0 * hours

If we want to create a new student, we would create an instance of the class Student which is an object of type Student:

Student willie = new Student("Willie", "Wildcat");
willie: Student = Student("Willie", "Wildcat")

Hence, the type of an object is the class it is an instance of. This is a staple across all object-oriented languages.

Static vs. Dynamic Typed Languages

A final note on types. You may hear languages being referred to as statically or dynamically typed. A statically typed language is one where the type is set by the code itself, either explicitly like Java:

int foo = 5;

or implicitly, where the compiler or interpreter determines the type based on the value, as in this statement from C# using the special var type:

var bar = 6;

In a statically typed language, a variable cannot be assigned a value of a different type, i.e.:

foo = 8.3;

Will fail with an error in Java, as a floating point value is a different type than an integer. However, we can cast the value to a new type (changing how it is represented), i.e.:

int x = (int)8.9;
x: int = int(8.9)

For this to work, the language must know how to perform the cast. The cast may also lose some information - in the above example, the resulting value of x is 8 (the fractional part is discarded).

In contrast, in a dynamically typed language the type of the variable changes when a value of a different type is assigned to it. For example, in Python, this expression is legal:

Python
a = 5
a = "foo"

and the type of a changes from an integer (at the first assignment) to string (at the second assignment).

C#, Java, C, C++, and Kotlin are all statically typed languages, while Python, JavaScript, and Ruby are dynamically typed languages.

Subsections of Types

Interfaces

YouTube Video

Video Materials

If we think back to the concept of message passing in object-oriented languages, it can be useful to think of the collection of public methods available in a class as an interface, i.e., a list of messages you can dispatch to an object created from that class. When you were first learning a language (and probably even now), you find yourself referring to these kinds of lists, usually in the language’s documentation:

Java API

Java API Java API

Python API

Python API Python API

Essentially, programmers use these interfaces to determine what methods can be invoked on an object. In other words, which messages can be passed to the object. This interface is determined by the class definition, specifically what methods it contains.

In dynamically typed programming languages, like Python, JavaScript, and Ruby, if two classes accept the same message, you can treat them interchangeably, i.e. if the Kangaroo class and Car class both define a jump() method, you could populate a list with both, and call the jump() method on each:

jumpables = [new Kangaroo(), new Car(), new Kangaroo()]
for jumper in jumpables:
    jumper.jump()

This is sometimes called duck typing , from the sense that “if it walks like a duck, and quacks like a duck, it might as well be a duck.”

However, for statically typed languages we must explicitly indicate that two types both possess the same message definition, by making the interface explicit. We do this by declaring an interface class, which is a special type of class. For example, an interface for classes that possess a parameter-less jump method might look like this in Java:

interface IJumpable {
    void jump();
}

In some languages, it is common practice to preface Interface names with the character I. The interface declaration defines an interface - the shape of the messages that can be passed to an object implementing the interface - in the form of a method signature. Note that this signature does not include a body, but instead ends in a semicolon (;). An interface simply indicates the message to be sent, not the behavior it will cause! We can specify as many methods in an interface declaration as we want.

Python Interfaces

On a later page, we’ll discuss how to create a similar structure in Python, which defines the methods that must be implemented by any class that inherits from our interface class. For now, we’ll discuss how interfaces are traditionally implemented in most other object-oriented languages such as Java.

Also note that the method signatures in an interface declaration do not have access modifiers. This is because the whole purpose of defining an interface is to signify methods that can be used by other code. In other words, public access is implied by including the method signature in the interface declaration. In addition, because the methods do not have implementations, they are also abstract as well.

This interface can then be implemented by other classes, usually by listing the interfaces as part of the class declaration. In most languages, a class may implement multiple interfaces. When a class implements an interface, it must define public methods with signatures that match those that were specified by the interface(s) it implements. Here’s an example of a couple of classes implementing the IJumpable interface in Java:

public class Kangaroo implements IJumpable {
    public void jump() {
        // implement method to jump over a fence here 
    }
}
public class Car implements IJumpable {
    public void jump() {
        // implement method to jumpstart a car here
    }
    
    public void start() {
        // implement method to normally start a car here
    }
}

We can then treat these two disparate classes as though they shared the same type, defined by the IJumpable interface:

List<IJumpable> jumpables = new LinkedList<>();
jumpables.add(new Kangaroo());
jumpables.add(new Car());
jumpables.add(new Kangaroo());
for(IJumpable jumper : jumpables) {
    jumper.jump();
}

Note that while we are treating the Kangaroo and Car instances as IJumpable instances, we can only invoke the methods defined in the IJumpable interface, even if these objects have other methods. Essentially, the interface represents a new type that can be shared amongst disparate objects in a statically-typed language. The interface definition serves to assure the static type checker that the objects implementing it can be treated as this new type - i.e. the interface provides a mechanism for implementing polymorphism.

We often describe the relationship between the interface and the class that implements it as a is-a relationship, i.e. a Kangaroo is a IJumpable (i.e. a Kangaroo is a thing that can jump). We further distinguish this from a related polymorphic mechanism, inheritance, by the strength of the relationship. We consider interfaces weak is-a connections, as other than the shared interface, a Kangaroo and a Car don’t have much to do with one another.

In Java, like most other object-oriented languages, a class can implement as many interfaces as we want, they just need to be separated by commas, i.e.:

public class Frog implements IJumpable, ICroakable, ICatchFlies {
    // method here
}

On the next few pages, we’ll look at how to implement interfaces more explicitly in both Java and Python. As always, feel free to read the page for the language you are studying, but it might be useful to review the other page as well. Then, we’ll look at inheritance, which represents a strong is-a relationship.

Subsections of Interfaces

Java Interfaces

YouTube Video

Video Materials

The Java programming language includes direct support for the creation of interfaces via the interface keyword. We’ve already seen one example of an interface created in Java, but let’s look at another example and dissect it a bit.

Interface Example

Here is a simple interface for a set of classes that are based on the Collection interface in Java 8:

public interface IMyCollection {
    int size();
    boolean isEmpty();
    boolean add(Object o);
    boolean remove(int i);
    Object get(int i);
    boolean contains(Object o);
}

You may also review the full Collection interface source code from the OpenJDK library.

Here’s another example interface in Java for a Stack:

public interface IMyStack {
    void push(Object o);
    Object pop();
    Object peek();
}

When creating an interface in Java, there are a few things to keep in mind:

  • Instead of the class keyword, we use the interface keyword in our declaration.
  • Interfaces usually only contain methods, but may contain attributes.
  • Any methods are automatically public and abstract. We do not have to include those keywords in the method declaration.
  • Any attributes are automatically public, static, and final. They are generally used for constants.
  • Interfaces cannot contain a constructor, and are not able to be directly instantiated. They are a special case of an abstract class.
  • Interface methods do not include any code. Instead of a set of curly braces {}, they end with a semicolon ;.

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:

public class MyList implements IMyCollection {
    
    private Object[] list;
    private int size;
    
    public List() {
        this.list = new Object[10];
        this.size = 0;
    }

    public int size() {
        return this.size;
    }
    
    public boolean isEmpty() {
        return this.size == 0;
    }
    
    public boolean add(Object o) {
        if (this.size < 10) {
            this.list[this.size++] = o;
            return true;
        }
        return false;
    }
    
    public boolean remove(int i) {
        if (i < 10) {
            this.list[i] = this.list[9];
            this.list[9] = null;
            size--;
            return true;
        }
        return false;
    }
    
    public Object get(int i) {
        return this.list[i];
    }
    
    public boolean contains(Object o) {
        for (Object obj : this.list) {
            if (obj.equals(o)) {
                return true;
            }
        }
        return false;
    }
}

Notice that we use the implements keyword as part of the class declaration to list 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 attributes and a constructor, as well as additional methods as needed.

Multiple Inheritance

One of the biggest benefits of using interfaces in Java is the ability to create a class that implements multiple interfaces. This is a special case 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:

public class MyListStack implements IMyCollection, IMyStack {

    // include all of the code from the MyList class
    
    public void push(Object o) {
        this.add(o);
    }
    
    public Object pop() {
        Object out = this.list[this.size - 1];
        this.remove(this.size - 1);
        return out;
    }
    
    public Object peek(){
        return this.list[this.size - 1];
    }
}

To implement multiple interfaces, we can simply list them following the implements keyword, 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 store classes that implement the same interface together. Here’s an example:

IMyCollection[] collects = new IMyCollection[2];
collects[0] = new MyList();
collects[1] = new MyListStack();
collects[0].add("String");
collects[1].add("Hello");

However, it is important to remember that, even though the second element in the collects array is an instance of the MyListStack class, we cannot access the push and pop methods directly. This is because the collects array is using the IMyCollection data type. So, we only have access to methods that are defined in that interface. Put another way, we’ve told the Java compiler that those objects can only accept those messages.

If we want to treat that item as an instance of the MyListStack class, we can cast it to the correct type.

if (collects[1] instanceof MyListStack) {
    ((MyListStack) collects[1]).push("World");
}

In Java, we can use the instanceof operator to determine if a particular object is an instance of a particular class or data type. If so, we can then cast it by placing the desired data type in parentheses before the variable we’d like to cast. In this example, we see that we can then wrap that in another set of parentheses and then access the methods or attributes of the desired type.

References

Subsections of Java Interfaces

Java Inheritance

In an object-oriented language, inheritance is a mechanism for deriving part of a class definition from another existing class definition. This allows the programmer to “share” code between classes, reducing the amount of code that must be written.

Consider the Student class we created earlier:

public class Student {
    
    private int creditPoints;
    private int creditHours;
    private String first;
    private String last;
    
    // accessor methods for first and last omitted

    public Student(String first, String last) {
        this.first = first;
        this.last = last;
    }
    
    /**
     * Gets the student's grade point average.
     */
    public double getGPA() {
        return ((double) creditPoints) / creditHours;
    }
    
    /**
     * Records a final grade for a course taken by this student.
     * 
     * @param grade       the grade earned by the student
     * @param hours       the number of credit hours in the course
     */
    public void addCourseGrade(Grade grade, int hours) {
        this.creditHours += hours;
        switch(grade) {
            case A:
                this.creditPoints += 4 * hours;
                break;
            case B:
                this.creditPoints += 3 * hours;
                break;
            case C:
                this.creditPoints += 2 * hours;
                break;
            case D:
                this.creditPoints += 1 * hours;
                break;
            case F:
                this.creditPoints += 0 * hours;
                break;
        }
    }
}

This would work well for representing a student. But what if we are representing multiple kinds of students, like undergraduate and graduate students? We’d need separate classes for each, but both would still have names and calculate their GPA the same way. So, it would be handy if we could say “an undergraduate is a student, and has all the properties and methods a student has” and “a graduate student is a student, and has all the properties and methods a student has.” This is exactly what inheritance does for us, and we often describe it as an is-a relationship. We distinguish this from the interface mechanism we looked at earlier by saying it is a strong is-a relationship, as an Undergraduate student is, for all purposes, also a Student.

Let’s define an undergraduate student class:

public class UndergraduateStudent extends Student {
    
    public UndergraduateStudent(String first, String last) {
        super(first, last);
    }

}

In Java, we use the extends keyword to declare that a class is inheriting from another class. So, public class UndergraduateStudent extends Student indicates that UndergraduateStudent inherits from (is a) Student. Thus, it has the attributes first and last that are inherited from Student. Similarly, it inherits the getGPA() and addCourseGrade() methods.

In fact, the only method we need to define in our UndergraduateStudent class is the constructor - and we only need to define this because the base class has a defined constructor taking two parameters, first and last names. This Student constructor must be invoked by the UndergraduateStudent constructor - that’s what the super(first, last) line does - it invokes the Student constructor with the first and last parameters passed into the UndergraduateStudent constructor. In Java, the super() method call must be the first line in the child class’s constructor. It can be omitted if the parent class includes a default (parameter-less) constructor.

Inheritance, State, and Behavior

Let’s define a GraduateStudent class as well. This will look much like an UndergraduateStudent, but all graduates have a bachelor’s degree:

public class GraduateStudent extends Student {

    private String bachelorDegree;
    
    public GraduateStudent(String first, String last, String degree) {
        super(first, last);
        this.bachelorDegree = degree;
    }
    
    public String getBachelorDegree() {
        return this.bachelorDegree;
    }

}

Here we added a property for bachelorDegree. Since the attribute itself is marked as private, it can only be written to by the class, as is done in the constructor. To the outside world, it is treated as read-only through the getter method.

Thus, the GraduateStudent has all the state and behavior encapsulated in Student, plus the additional state of the bachelor’s degree title.

The protected Keyword

What you might not expect is that any fields declared private in the base class are inaccessible in the derived class. Thus, the private fields creditPoints and creditHours cannot be used in a method defined in GraduateStudent. This is again part of the encapsulation and data hiding ideals - we’ve encapsulated and hid those variables within the base class, and any code outside that assembly, even in a derived class, is not allowed to mess with it.

However, we often will want to allow access to such variables in a derived class. Java uses the access modifier protected to allow for this access in derived classes, but not the wider world.

In UML, protected attributes are denoted by a hash symbol # as the visibility of the attribute.

Inheritance and Memory

What happens when we construct an instance of GraduateStudent? First, we invoke the constructor of the GraduateStudent class:

GraduateStudent gradStudent = new GraduateStudent("Willie", "Wildcat", "Computer Science");

This constructor then invokes the constructor of the base class, Student, with the arguments "Willie" and "Wildcat". Thus, we allocate space to hold the state of a student, and populate it with the values set by the constructor. Finally, execution returns to the super class of GraduateStudent, which allocates the additional memory for the reference to the BachelorDegree property. Thus, the memory space of the GraduateStudent contains an instance of the Student, somewhat like nesting dolls.

Because of this, we can treat a GraduateStudent object as a Student object. For example, we can store it in a list of type Student, along with UndergraduateStudent objects:

List<Student> students = new LinkedList<>();
students.Add(gradStudent);
students.Add(new UndergraduateStudent("Dorothy", "Gale"));

Because of their relationship through inheritance, both GraduateStudent class instances and UndergraduateStudent class instances are considered to be of type Student, as well as their supertypes.

Nested Inheritance

We can go as deep as we like with inheritance - each base type can be a superclass of another base type, and has all the state and behavior of all the inherited base classes.

This said, having too many levels of inheritance can make it difficult to reason about an object. In practice, a good guideline is to limit nested inheritance to two or three levels of depth.

Abstract Classes

If we have a base class that only exists to be inherited from (like our Student class in the example), we can mark it as abstract with the abstract keyword. An abstract class cannot be instantiated (that is, we cannot create an instance of it using the new keyword). It can still define fields and methods, but you can’t construct it. If we were to re-write our Student class as an abstract class:

public abstract class Student {
    
    private int creditPoints;
    private int creditHours;
    protected String first;
    protected String last;
    
    // accessor methods for first and last omitted

    public Student(String first, String last) {
        this.first = first;
        this.last = last;
    }
    
    /**
     * Gets the student's grade point average.
     */
    public double getGPA() {
        return ((double) creditPoints) / creditHours;
    }
    
    /**
     * Records a final grade for a course taken by this student.
     * 
     * @param grade       the grade earned by the student
     * @param hours       the number of credit hours in the course
     */
    public void addCourseGrade(Grade grade, int hours) {
        this.creditHours += hours;
        switch(grade) {
            case A:
                this.creditPoints += 4 * hours;
                break;
            case B:
                this.creditPoints += 3 * hours;
                break;
            case C:
                this.creditPoints += 2 * hours;
                break;
            case D:
                this.creditPoints += 1 * hours;
                break;
            case F:
                this.creditPoints += 0 * hours;
                break;
        }
    }
}

Now with Student as an abstract class, attempting to create a Student instance:

Student theWiz = new Student("Wizard", "Oz");

would fail with an exception. However, we can still create instances of the derived classes GraduateStudent and UndergraduateStudent, and treat them as Student instances. It is best practice to make any class that serves only as a base class for derived classes and will never be created directly an abstract class.

Sealed Classes

Some programming languages, such as C#, include a special keyword sealed that can be added to a class declaration. A sealed class is not inheritable, so no other classes can extend it. This further adds security to the programming model by preventing developers from even creating their own version of that class that would be compatible with the original version.

This is currently a proposed feature for Java version 15. The full details of that proposed feature are described in the Java Language Updates from Oracle.

Since we are focusing on learning Java that is compatible with Java 8, we won’t have access to that feature.

Interfaces and Inheritance

A class can use both inheritance and interfaces. In Java, a class can only inherit one base class, and it should always be listed first after the extends keyword. Following that, we can have as many interfaces as we want listed after the implements keyword, all separated from each other and the base class by commas (,):

public class UndergraduateStudent extends Student implements ITeachable, IEmailable {
  // TODO: Implement student class 
}

Python Interfaces

YouTube Video

Video Materials

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 the typing library to assist with some type checking.
  • In the class definition for our IMyCollection class, we are listing the abc.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 Python issubclass 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 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

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

Subsections of Python Interfaces

Python Inheritance

In an object-oriented language, inheritance is a mechanism for deriving part of a class definition from another existing class definition. This allows the programmer to “share” code between classes, reducing the amount of code that must be written.

Consider the Student class we created earlier:

class Student:

    def __init__(self, first: str, last: str) -> None:
        self.__first: str = first
        self.__last: str = last
        self.__credit_points: int = 0
        self.__credit_hours: int = 0
        
    # properties for first and last omitted
    
    @property
    def gpa(self) -> float:
        """Gets the student's grade point average.
        """
        return self.__credit_points / self.__credit_hours
    
    def add_course_grade(self, grade: Grade, hours: int) -> None:
        """Records a final grade for a course taken by this student.
        
        Args
           grade: the grade earned by the student
           hours: the number of credit hours in the course
        """
        self.__credit_hours += hours
        if grade == Grade.A:
            self.__credit_points += 4 * hours
        elif grade == Grade.B:
            self.__credit_points += 3 * hours
        elif grade == Grade.C:
            self.__credit_points += 2 * hours
        elif grade == Grade.D:
            self.__credit_points += 1 * hours
        elif grade == Grade.F:
            self.__credit_points += 0 * hours

This would work well for representing a student. But what if we are representing multiple kinds of students, like undergraduate and graduate students? We’d need separate classes for each, but both would still have names and calculate their GPA the same way. So, it would be handy if we could say “an undergraduate is a student, and has all the properties and methods a student has” and “a graduate student is a student, and has all the properties and methods a student has.” This is exactly what inheritance does for us, and we often describe it as an is-a relationship. We distinguish this from the interface mechanism we looked at earlier by saying it is a strong is-a relationship, as an Undergraduate student is, for all purposes, also a Student.

Let’s define an undergraduate student class:

class UndergraduateStudent(Student):

    def __init__(self, first: str, last: str) -> None:
        super().__init__(first, last)

In Python, we list the classes that a new class is inheriting from in parentheses at the end of the class definition. So, class UndergraduateStudent(Student): indicates that UndergraduateStudent inherits from (is a) Student. Thus, it has the attributes first and last that are inherited from Student, as well as the gpa property. Similarly, it inherits the add_course_grade() method.

In fact, the only method we need to define in our UndergraduateStudent class is the constructor - and we only need to define this because the base class has a defined constructor taking two parameters, first and last names. This Student constructor must be invoked by the UndergraduateStudent constructor - that’s what the super().__init__(first, last) line does - it invokes the Student constructor with the first and last parameters passed into the UndergraduateStudent constructor. In Python, the super() method call is usually the first line in the child class’s constructor, but it doesn’t have to be. It can be omitted if the parent class includes a default (parameter-less) constructor.

Inheritance, State, and Behavior

Let’s define a GraduateStudent class as well. This will look much like an UndergraduateStudent, but all graduates have a bachelor’s degree:

class GraduateStudent(Student):
    
    def __init__(self, first: str, last: str, degree: str) -> None:
        super().__init__(first, last)
        self.__bachelor_degree = degree
        
    @property
    def bachelor_degree(self) -> str:
        return self.__bachelor_degree

Here we added a property for bachelor_degree. Since the attribute itself is meant to be a private attribute (the name begins with two underscores __), it should only be written to by the class, as is done in the constructor. To the outside world, it is treated as read-only through the getter method. Of course, in Python, nothing is truly private, so a determined developer can always access these attributes if desired.

Thus, the GraduateStudent has all the state and behavior encapsulated in Student, plus the additional state of the bachelor’s degree title.

Protected Attributes

What you might not expect is that any fields that are private in the base class are inaccessible in the derived class. This is due to the way that Python performs name mangling of names that begin with two underscores __. Thus, the private fields credit_points and credit_hours cannot be used in a method defined in GraduateStudent. This is again part of the encapsulation and data hiding ideals - we’ve encapsulated and hid those variables within the base class, and any code outside that assembly, even in a derived class, is not allowed to mess with it.

However, we often will want to allow access to such variables in a derived class. In Python, we can use a single underscore _ in front of a variable or method name to indicate that it should be treated like a protected attribute, which is only accessed by the class that defines it and any classes that inherit from that class. However, as with anything else in Python, this attribute will still be accessible to any code within our program, so it is up to developers to respect the naming scheme and not try to access those directly.

In UML, protected attributes are denoted by a hash symbol # as the visibility of the attribute.

Inheritance and Memory

What happens when we construct an instance of GraduateStudent? First, we invoke the constructor of the GraduateStudent class:

grad_student: GraduateStudent = GraduateStudent("Willie", "Wildcat", "Computer Science")

This constructor then invokes the constructor of the base class, Student, with the arguments "Willie" and "Wildcat". Thus, we allocate space to hold the state of a student, and populate it with the values set by the constructor. Finally, execution returns to the super class of GraduateStudent, which allocates the additional memory for the reference to the bachelor_degree property. Thus, the memory space of the GraduateStudent contains an instance of the Student, somewhat like nesting dolls.

Because of this, we can treat a GraduateStudent object as a Student object. For example, we can store it in a list that contains Student instances, along with UndergraduateStudent objects:

students: List[Student] = list()
students.append(grad_student)
students.append(UndergraduateStudent("Dorothy", "Gale"))

Because of their relationship through inheritance, both GraduateStudent class instances and UndergraduateStudent class instances are considered to be of type Student, as well as their supertypes.

Nested Inheritance

We can go as deep as we like with inheritance - each base type can be a superclass of another base type, and has all the state and behavior of all the inherited base classes.

This said, having too many levels of inheritance can make it difficult to reason about an object. In practice, a good guideline is to limit nested inheritance to two or three levels of depth.

Abstract Classes

If we have a base class that only exists to be inherited from (like our Student class in the example), we can mark it as abstract by inheriting from the ABC class. ABC is short for abstract base class. An abstract class cannot be instantiated (that is, we cannot create an instance of it by calling its constructor) unless all of its abstract methods have been overridden. It can still define fields and methods, but you can’t construct it. If we were to re-write our Student class as an abstract class:

from abc import ABC


class Student(ABC):
    
    def __init__(self, first: str, last: str) -> None:
        self.__first: str = first
        self.__last: str = last
        self.__credit_points: int = 0
        self.__credit_hours: int = 0
        
    # properties for first and last omitted
    
    @property
    def gpa(self) -> float:
        """Gets the student's grade point average.
        """
        return self.__credit_points / self.__credit_hours
    
    def add_course_grade(self, grade: Grade, hours: int) -> None:
        """Records a final grade for a course taken by this student.
        
        Args
           grade: the grade earned by the student
           hours: the number of credit hours in the course
        """
        self.__credit_hours += hours
        if grade == Grade.A:
            self.__credit_points += 4 * hours
        elif grade == Grade.B:
            self.__credit_points += 3 * hours
        elif grade == Grade.C:
            self.__credit_points += 2 * hours
        elif grade == Grade.D:
            self.__credit_points += 1 * hours
        elif grade == Grade.F:
            self.__credit_points += 0 * hours

Now with Student as an abstract class, attempting to create a Student instance:

the_wiz: Student = Student("Wizard", "Oz")

would still be allowed since our Student class does not define any abstract methods. However, we can add an abstract method, such as the student_type method shown below.

    @abstractmethod
    def student_type(self) -> str:
        raise NotImplementedError

If that method is placed within our Student class, we could no longer directly instantiate the class since it contains an abstract method. However, we can still create instances of the derived classes GraduateStudent and UndergraduateStudent, and treat them as Student instances, provided that they override the abstract method student_type in their code. It is best practice to make any class that serves only as a base class for derived classes and will never be created directly an abstract class.

Sealed Classes

Some programming languages, such as C#, include a special keyword sealed that can be added to a class declaration. A sealed class is not inheritable, so no other classes can extend it. This further adds security to the programming model by preventing developers from even creating their own version of that class that would be compatible with the original version.

This could theoretically be done in Python through the use of metaprogramming. However, due to the fact that no attributes or methods are truly private in Python, it wouldn’t have the desired effect of preventing other classes from gaining access to protected attributes and methods. So, we won’t cover how to do this here.

Interfaces and Inheritance

A class can use both inheritance and interfaces. In Python, a class can inherit multiple base classes, either as interfaces or as true parent classes. They work the same way - how the class is handled really depends on the code in the class that is being inherited.

class UndergraduateStudent(Student, ITeachable, IEmailable):

For more on multiple inheritance in Python, check out the Multiple Inheritance in Python article from Real Python.

Type Checking & Conversion

You have probably used casting to convert numeric values from one type to another, i.e.:

double a = 5.5;
int b = (int) a;
a: float = 5.5
b: int = int(a)

What you are actually doing when you cast is transforming a value from one type to another. In the first case, you are taking the value of a, which is the floating-point value 5.5, and converting it to the equivalent integer value 5.

Both of these are examples of an explicit cast, since we are explicitly stating the type that we’d like to convert our existing value to.

In some languages, we can also perform an implicit cast. This is where the compiler or interpreter changes the type of our value behind the scenes for us.

int a = 5;
double b = a + 2.5;
a: int = 5
b: float = a + 2.5;

In these examples, the integer value stored in a is implicitly converted to the floating point value 5.0 before it is added to 2.5 to get the final result. This conversion is done automatically for us.

However, as we’ve observed already, each language has some special cases where implicit casting is not allowed. In general, if the implicit cast will result in loss of data, such as when a floating-point value is converted to an integer, we must use an explicit cast instead.

Casting and Inheritance

Casting becomes a bit more involved when we consider inheritance. As you saw in the previous discussion of inheritance, we can treat derived classes as the base class. For example, in Java, the code:

Student willie = new UndergraduateStudent("Willie", "Wildcat");

is actually implicitly casting the UndergraduateStudent object “Willie Wildcat” into a Student class. Because an UndergraduateStudent is a Student, this cast can be implicit. Going the other way requires an explicit cast as there is a chance that the Student we are casting isn’t an UndergraduateStudent, i.e.:

UndergraduateStudent u = (UndergraduateStudent)willie;

If we tried to cast willie into a graduate student:

GraduateStudent g = (GraduateStudent)willie;

The program would throw a ClassCastException when run.

In Python, things are a bit different. Recall that Python is a dynamically typed language. So, when we create an UndergraduateStudent object, the Python interpreter knows that that object has the type UndergraduateStudent. So, we can treat it as an instance of both the Student and UndergraduateStudent class. We don’t have to perform any conversions to do so.

However, if we try to treat it like an instance of the GraduateStudent class, it would fail with an AttributeError.

Checking Types

Both Java and Python include special methods for determining if a particular object is compatible with a certain type.

Student u = new UndergraduateStudent("Willie", "Wildcat");
if (u instanceof UndergraduateStudent) {
    UndergraduateStudent uGrad = (UndergraduateStudent) willie;
    // treat willie as an undergraduate student here
}
u: Student = UndergraduateStudent("Willie", "Wildcat")
if isinstance(u, UndergraduateStudent):
    # treat willie as an undergraduate student here

Java uses the instanceof operator to perform the check, while Python has a built-in isinstance method to perform the same task. Typically these statements are used as part of a conditional statement, allowing us to check if an object is compatible with a given type before we try to use that object in that way.

So, if we have a list of Student objects, we can use this method to determine if those objects are instances of UndergraduateStudent or GraduateStudent. It’s pretty handy!

Message Dispatching

YouTube Video

Video Materials

The term dispatch refers to how a language decides which polymorphic operation (a method or function) a message should trigger.

Consider polymorphic functions in Java, also known as method overloading, where multiple methods use the same name but have different parameters. Here’s an example for calculating the rounded sum of an array of numbers:

Java
public int roundedSum(int[] a){
    int sum = 0;
    for (int i : a) {
        sum += i;
    }
    return sum
}

public int roundedSum(double[] a){
    double sum = 0;
    for (double i : a) {
        sum += i;
    }
    return Math.round(sum);
}

How does the computer know which version to invoke at runtime? It should not be a surprise that it is determined by the arguments - if an integer array is passed, the first is invoked, if a float array is passed, the second.

Python works a bit differently. In Python, method overloading is not allowed, so there cannot be two methods with the same name within a class. To achieve the same effect, optional parameters are used. In addition, because Python is dynamically typed, we could instead write our function to accept values of multiple types:

Python
def rounded_sum(a: List[Union[int, float]]) -> int:
    sum_value: float = 0.0
    for i in a:
        sum_value += i
    return round(sum_value)

As we can see, that function will accept a list of either integer values or floating-point values, and it can properly handle them in either case. In Python, the name of the method is the only thing that is used to determine which piece of code should be executed, not the arguments.

Object-Oriented Polymorphism

However, inheritance can cause some challenges in selecting the appropriate polymorphic form. Consider the following fruit implementations that feature a blend() method:

public class Fruit {

    public String blend() {
        return "A pulpy mess, I guess";
    }
}

public class Banana extends Fruit {

    @Override
    public String blend() {
        return "Yellow mush";
    }
}

public class Strawberry extends Fruit {

    @Override
    public String blend() {
        return "Gooey red sweetness!";
    }
}
class Fruit:
    
    def blend(self) -> str:
        return "A pulpy mess, I guess"

    
class Banana(Fruit):
    
    def blend(self) -> str:
        return "Yellow mush"
    

class Strawberry(Fruit):
    
    def blend(self) -> str:
        return "Gooey red sweetness!"

Let’s add some fruit instances to a list, and invoke their blend() methods:

LinkedList<Fruit> toBlend = new LinkedList<>();
toBlend.add(new Fruit());
toBlend.add(new Banana());
toBlend.add(new Strawberry());
for(Fruit f : toBlend){
    System.out.println(f.blend());
}
to_blend: List[Fruit] = list()
to_blend.append(Fruit())
to_blend.append(Banana())
to_blend.append(Strawberry())
for f in to_blend:
    print(f.blend())

What will be printed? If we look at the declared types, we’d expect each of them to act like a Fruit instance, so in that case the output would be just three lines of A pulpy mess, I guess?

However, that is not correct! This is the powerful aspect of polymorphic method dispatch. In both Java and Python, we don’t look at the declared type of the object, but the actual underlying type of the instance itself. So, if the object was created as a Banana or Strawberry, then it will use the overridden methods from those child classes instead of the parent Fruit class. So, the actual output we’ll get is:

A pulpy mess, I guess
Yellow mush
Gooey red sweetness!

In both Java and Python, we see an example of method overriding. If we include a method of the same name in the child class (and the same set of parameters, in the case of Java), we can override the method that exists in the parent class. In Java, we must use the @Override decorator, but Python doesn’t require anything special.

Abstract vs. Interface

Of course, we can also update this example to either use an abstract class or an interface. There are some pros and cons to either option, but here’s a good rule of thumb to start with:

  • Use inheritance without making the parent class abstract only if it makes sense for the parent class to be instantiated itself. So, it might make sense to have a parent Car class and a subclass SportsCar that are both able to be instantiated.
  • Use inheritance with abstract classes if the parent class should not be instantiated. For example, when modeling the animal kingdom with a parent Canine class and subclasses Dog and Wolf, it might be best if the parent class cannot be instantiated directly.
  • Use interfaces when you want to design a set of methods or behaviors that a class should implement, but which may not otherwise create strong a relationship between the classes. For example, we could create an IUpdatable interface to require several classes to implement a method called update, but the classes themselves might not be related otherwise.

Finally, remember that there are not really any correct answers here - each option comes with trade-offs, and it is up to you as a developer to help determine which is best. Therefore, it is very helpful to have experience with all three approaches so you understand how each one can be used.

Subsections of Message Dispatching

Summary

In this chapter, we explored the concept of types and discussed how variables are specific types that can be explicitly or implicitly declared. We saw how in a statically-typed language (like Java), variables are not allowed to change types, though they can do so in a dynamically-typed language like Python. We also discussed how casting can convert a value stored in a variable into a different type. Implicit casts can happen automatically, but explicit casts must be indicated by the programmer using a cast operator, as the cast could result in loss of precision or the throwing of an exception.

We explored how class declarations and interface declarations create new types. We saw how polymorphic mechanisms like interface implementation and inheritance allow object to be treated as (and cast to) different types. We also introduced a few casting operators, which can be used to cast or test the ability to cast.

Finally, we explored how messages are dispatched when polymorphism is involved. We saw that the method invoked depends on what type the object was created as, not the type it is currently stored within.

Review Quiz

Check your understanding of the new content introduced in this chapter below - this quiz is not graded and you can retake it as many times as you want.

Quizdown quiz omitted from print view.