# 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):

```python
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.

```python
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:

```python
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:

```python
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:

```python
>>> 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`:

```python
>>> # 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.

```python
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:

```python
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 {download}`the code that we've written so far <code/employee_v1.py>`.
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`.


[^1]: While we won't follow it in this course, a common Python convention you'll encounter is to put the word "abstract" in the class name, e.g `AbstractEmployee`.
[^2]: For now, we will hard-code values in the method, but will generalize this later.
[^3]: Good thing, because it raises an error!
