3.5 Inheritance: Introduction and Methods#

In this and the next few sections, we’ll learn about a relationship called inheritance that can exist between two classes. We will focus on one particular way of using inheritance in our code design, and through that, will learn how inheritance works in Python.

Consider a payroll system#

Suppose we are designing an application to keep track of a company’s employees, including some who are paid based on an annual salary and others who are paid based on an hourly wage. We might choose to define a separate class for these two types of employee so that they could have different attributes. For instance, only a salaried employee has a salary to be stored, and only an hourly-paid employee has an hourly wage to be recorded. The classes could also have different methods for the different ways that their pay is computed.

This design overlooks something important: employees of both types have many things in common. For instance, they all have data like a name, address, and employee id. And even though their pay is computed differently, they all get paid. If we had two classes for the two kinds of employees, all the code for these common elements would be duplicated. This is not only redundant but error prone: if you find a bug or make another kind of improvement in one class, you may forget to make the same changes in the other class. Things get even worse if the company decides to add other kinds of employees, such as those working on commission.

Inheritance to the rescue!#

A better design would “factor out” the things that are common to all employees and write them once. This is what inheritance allows us to do, and here is how:

  • We define a base class that includes the functionality that are common to all employees.

  • We define a subclass for each specific type of employee. In each subclass, we declare that it is a kind of employee, which will cause it to “inherit” those common elements from the base class without having to define them itself.

    Terminology note: if class B is a subclass of class A, we also say that A is a superclass of B.

In our running example of a company with different kinds of employees, we could define a base class Employee and two subclasses as follows (for now, we will leave the class bodies empty):

class Employee:
    pass

# We use the "(Employee)" part to mark SalariedEmployee as a subclass of Employee.
class SalariedEmployee(Employee):
    pass

# We use the "(Employee)" part to mark HourlyEmployee as a subclass of Employee.
class HourlyEmployee(Employee):
    pass

Inheritance terminology#

It’s useful to know that there are three ways to talk about classes that are in an inheritance relationship:

  • base class, superclass, and parent class are synonyms.

  • derived class, subclass, and child class are synonyms.

Defining methods in a base class#

Now let’s fill in these classes, starting with the methods we want. Once we have that, we can figure out the data that will be needed to implement the methods. To keep our example simple, let’s say that we only need a method for paying an employee, and that it will just print out a statement saying when and how much the employee was paid.

Here’s the outline for the Employee class with a pay method.

class Employee:
    """An employee of a company.
    """

    def pay(self, pay_date: date) -> None:
        """Pay this Employee on the given date and record the payment.

        (Assume this is called once per month.)
        """
        pass

If we try to write the body of the pay method, we run into a problem: it must compute the appropriate pay amount for printing, and that must be done differently for each type of employee. Our solution is to pull that part out into a helper method:

class Employee:
    """An employee of a company.
    """

    def get_monthly_payment(self) -> float:
        """Return the amount that this Employee should be paid in one month.

        Round the amount to the nearest cent.
        """
        pass  # We still have to figure this out.

    def pay(self, pay_date: date) -> None:
        """Pay this Employee on the given date and record the payment.

        (Assume this is called once per month.)
        """
        payment = self.get_monthly_payment()
        print(f'An employee was paid {payment} on {pay_date}.')

Now method pay is complete, but we have the same problem with get_monthly_payment: it has to be different for each type of employee. Clearly, the subclasses must define this method, each in their own way. But we are going to leave the incomplete get_monthly_payment method in the Employee class, because it defines part of the interface that every type of Employee object needs to have. Subclasses will inherit this incomplete method, which they can redefine as appropriate. We’ll see how to do that shortly.

Notice that we did as much as we could in the base class, to avoid repeating code in the subclasses.

Making the base class abstract#

Because the Employee class has a method with no body, client code should not make instances of this incomplete class directly. We do two things to make this clear:

  • Change the body of the incomplete method so that it simply raises a NotImplementedError. We call such a method an abstract method, and we call a class which has at least one abstract method an abstract class.

  • Add a comment to the class docstring stating that the class is abstract, so that anyone writing client code is warned not to instantiate it.[1]

Here is the complete definition of our class:

class Employee:
    """An employee of a company.

    This is an abstract class. Only subclasses should be instantiated.
    """
    def get_monthly_payment(self) -> float:
        """Return the amount that this Employee should be paid in one month.

        Round the amount to the nearest cent.
        """
        raise NotImplementedError

    def pay(self, pay_date: date) -> None:
        """Pay this Employee on the given date and record the payment.

        (Assume this is called once per month.)
        """
        payment = self.get_monthly_payment()
        print(f'An employee was paid {payment} on {pay_date}.')

It is possible for client code to ignore the warning and instantiate this class—Python does not prevent it. But look at what happens when we try to call one of the unimplemented methods on the object:

>>> a = Employee()
>>> # This method is itself abstract:
>>> a.get_monthly_payment()
Traceback...
NotImplementedError
>>> # This method calls a helper method that is abstract:
>>> a.pay(date(2018, 9, 30))
Traceback...
NotImplementedError

Subclasses inherit from the base class#

Now let’s fill in class SalariedEmployee, which is a subclass of Employee. Very importantly, all instances of SalariedEmployee are also instances of Employee. We can verify this using the built-in function isinstance:

>>> # Here we see what isinstance does with an object of a simple built-in type.
>>> isinstance(5, int)
True
>>> isinstance(5, str)
False
>>> # Now let's check how it works with objects of a type that we define.
>>> fred = SalariedEmployee()
>>> # fred's type is as we constructed it: SalariedEmployee.
>>> # More precisely, the object that fred refers to has type SalariedEmployee.
>>> type(fred)
<class 'employee.SalariedEmployee'>
>>> # In other words, the object is an instance of SalariedEmployee.
>>> isinstance(fred, SalariedEmployee)
True
>>> # Here's the important part: it is also an instance of Employee.
>>> isinstance(fred, Employee)
True

Because Python “knows” that fred is an instance of Employee, this object will have access to all methods of Employee! We say that fred inherits all of the Employee methods. So even if SalariedEmployee remains an empty class, its instances can still call the methods get_monthly_payment and pay, because they are inherited.

class SalariedEmployee(Employee):
    pass

>>> fred = SalariedEmployee()
>>> # fred inherits Employee.get_monthly_payment, and so can call it.
>>> # Of course, it raises an error when called, but it indeed is accessed.
>>> fred.get_monthly_payment()
Traceback...
NotImplementedError

Completing the subclass#

Our SalariedEmployee and HourlyEmployee subclasses each inherit two methods: pay and get_monthly_payment. The method pay is complete as it is, and is appropriate for all types of employees, so we needn’t do anything with it. However, get_monthly_payment needs a new definition that does not raise an error and that defines the behaviour appropriately for the particular kind of employee. We accomplish this simply by defining the method again in the subclass.[2] We say that this new method definition overrides the inherited definition:

class SalariedEmployee(Employee):
    def get_monthly_payment(self) -> float:
        # Assuming an annual salary of 60,000
        return round(60000.0 / 12.0, 2)

class HourlyEmployee(Employee):
    def get_monthly_payment(self) -> float:
        # Assuming a 160-hour work month and a $20/hour wage.
        return round(160.0 * 20.0, 2)

>>> fred = SalariedEmployee()
>>> fred.get_monthly_payment()
5000.0
>>> jerry = HourlyEmployee()
>>> jerry.get_monthly_payment()
3200.0

We now have a working version of all three classes, albeit a very limited one. Download and run the code that we've written so far. You can experiment with it as you continue reading.

How Python resolves a method name#

The interaction above includes the call fred.get_monthly_payment(). Since the name get_monthly_payment could refer to several possible methods (one in class Employee, one in class SalariedEmployee, and one in class HourlyEmployee), we say that the method name must be “resolved”. To understand inheritance, we need to know how Python handles method resolution in general.

This is how it works: whenever code calls a.myMethod(), Python determines what type(a) is and looks in that class for myMethod. If myMethod is found in this class, that method is called; otherwise, Python next searches for myMethod in the superclass of the type of a, and then the superclass of the superclass, etc., until it either finds a definition of myMethod or it has exhausted all possibilities, in which case it raises an AttributeError.

In the case of the call fred.get_monthly_payment(), type(fred) is SalariedEmployee, and SalariedEmployee contains a get_monthly_payment method. So that is the one called.

This method call is more interesting: fred.pay(date(2018, 9, 30)). The value of type(fred) is SalariedEmployee, but class SalariedEmployee does not contain a pay method. So Python next checks in the superclass Employee, which does contain a pay method, so then that is called. Straightforward. But then inside Employee.pay, we have the call self.get_monthly_payment(). Which get_monthly_payment is called? We’re already executing a method (pay) inside the Employee class, but that doesn’t mean we call Employee.get_monthly_payment.[3] Remember the rule: type(self) determines which class Python first looks in for the method. At this point, self is fred, whose type is SalariedEmployee, and that class contains a get_monthly_payment method. So in this case, when Employee.pay calls self.get_monthly_payment(), it gets SalariedEmployee.get_monthly_payment.