# 3.6 Inheritance: Attributes and Initializers

Let's return to {download}`the payroll code we wrote <code/employee_v1.py>`
and generalize it from hard-coded values to instance attributes.
This will allow us to customize individual employees with their own annual salaries or hourly wages.


## Documenting attributes

Just as the base class contains methods (even abstract ones!)
that all subclasses need to have in common,
the base class also documents attributes
that all subclasses need to have in common.
Both are a fundamental part of the public interface of a class.

We decided earlier that the application would need to record
an id and a name for all employees.
Here's how we document that in the base class:[^1]

```python
class Employee:
    """An employee of a company.

    Attributes:
        id_: This employee's ID number.
        name: This employee's name.
    """
    id_: int
    name: str
```

## Defining an initializer in the abstract superclass

Even though abstract classes should not be instantiated directly,
we provide an initializer in the superclass
to initialize the common attributes.

```python
class Employee:
    def __init__(self, id_: int, name: str) -> None:
        """Initialize this employee.
        """
        self.id_ = id_
        self.name = name

    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, date: str) -> 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 {date}.')
```

## Inheriting the initializer in a subclass

Because the initializer is a method,
it is automatically inherited by all `Employee` subclasses
just as, for instance, `pay` is.

```python
>>> # Assuming SalariedEmployee does not override Employee.__init__,
>>> # that method is called when we construct a SalariedEmployee.
>>> fred = SalariedEmployee(99, 'Fred Flintstone')
>>> # We can see that Employee.__init__ was called,
>>> # and the two instance attributes have been initialized.
>>> fred.name
'Fred Flintstone'
>>> fred.id_
99
```

Just as with all other methods, for each subclass, we must decide whether the inherited implementation is suitable for our class, or whether we want to override it.
In this case, the inherited initializer is not suitable, because each subclass requires that additional instance attributes be initialized:
For each `SalariedEmployee` we need to keep track of the employee's salary,
and for each `HourlyEmployee` we need to keep track of their number of work hours per week and their hourly wage.

Certainly we could override and replace the inherited initializer,
and in its body copy the code from `Employee.__init__`:

```python
class SalariedEmployee(Employee):
    def __init__(self, id_: int, name: str, salary: float) -> None:
        self.id_ = id_         # Copied from Employee.__init__
        self.name = name       # Copied from Employee.__init__
        self.salary = salary   # Specific to SalariedEmployee

class HourlyEmployee(Employee):
    def __init__(self, id_: int, name: str, hourly_wage: float,
                 hours_per_month: float) -> None:
        self.id_ = id_                          # Copied from Employee.__init__
        self.name = name                        # Copied from Employee.__init__
        self.hourly_wage = hourly_wage          # Specific to HourlyEmployee
        self.hours_per_month = hours_per_month  # Specific to HourlyEmployee
```

This is not a very satisfying solution because the first two lines of each initializer are duplicated---and for more complex abstract base classes, the problem would be even worse!

Since the inherited initializer does part of the work by initializing the attributes that all employees have in common, we can instead use `Employee.__init__` as a helper method.
In other words, rather than override and replace this method,
we will override and *extend* it.
As we saw briefly last week, we use the superclass name to access
its method:[^2]

```python
class SalariedEmployee(Employee):
    def __init__(self, id_: int, name: str, salary: float) -> None:
        # Note that to call the superclass initializer, we need to use the
        # full method name '__init__'. This is the only time you should write
        # '__init__' explicitly.
        Employee.__init__(self, id_, name)
        self.salary = salary
```

In the subclasses, we need to document each instance new attribute and declare its type.
Here are the complete subclasses:

```python
class SalariedEmployee(Employee):
    """
    Attributes:
        salary: This employee's annual salary

    Representation Invariants:
    - self.salary >= 0
    """
    salary: float

    def __init__(self, id_: int, name: str, salary: float) -> None:
        # Note that to call the superclass initializer, we need to use the
        # full method name '__init__'. This is the only time you should write
        # '__init__' explicitly.
        Employee.__init__(self, id_, name)
        self.salary = salary

    def get_monthly_payment(self) -> float:
        return round(self.salary / 12, 2)


class HourlyEmployee(Employee):
    """An employee whose pay is computed based on an hourly rate.

    Attributes:
        hourly_wage:
            This employee's hourly rate of pay.
        hours_per_month:
            The number of hours this employee works each month.

    Representation Invariants:
    - self.hourly_wage >= 0
    - self.hours_per_month >= 0
    """
    hourly_wage: float
    hours_per_month: float

    def __init__(self, id_: int, name: str, hourly_wage: float,
                 hours_per_month: float) -> None:
        Employee.__init__(self, id_, name)
        self.hourly_wage = hourly_wage
        self.hours_per_month = hours_per_month

    def get_monthly_payment(self) -> float:
        return round(self.hours_per_month * self.hourly_wage, 2)
```

We can see that when we construct an instance of either subclass,
both the common instance attributes
(`name` and `id_`)
and the subclass-specific attributes are initialized:

```python
>>> fred = SalariedEmployee(99, 'Fred Flintstone', 60000.0)
>>> fred.name
'Fred Flintstone'
>>> fred.salary
60000
>>> barney = HourlyEmployee(23, 'Barney Rubble', 1.25, 50.0)
>>> barney.name
'Barney Rubble'
>>> barney.hourly_wage
1.25
>>> barney.hours_per_month
50.0
```

We have now completed the {download}`second version of the code <code/employee_v2.py>`.
Download it so that you can experiment with it as you continue reading.


## Subclasses inherit methods, not attributes

It may seem that our two subclasses have "inherited" the attributes documented in the `Employee` class.[^3]
But remember that a type annotation does not create a variable.
Consider this example:

```python
>>> fred = SalariedEmployee(99, 'Fred Flintstone', 60000.0)
>>> fred.name
'Fred Flintstone'
```

The only reason that `fred` has a `name` attribute is because the `SalariedEmployee` initializer explicitly calls the `Employee` initializer, which initializes this attribute.
A superclass initializer is *not* called automatically when a subclass instance is created.
If we remove this call from our example, we see that the two attributes `name` and `id_` are missing:

```python
class SalariedEmployee(Employee):
    def __init__(self, id_: int, name: str, salary: float) -> None:
        # Superclass call commented out:
        # Employee.__init__(self, id_, name)
        self.salary = salary


>>> fred = SalariedEmployee('Fred Flintstone')
>>> fred.name
AttributeError
```


## Initializers with different signatures

Notice that the signatures for `Employee.__init__` and `SalariedEmployee.__init__`
are different.
`SalariedEmployee.__init__` has an additional parameter for the salary.
This makes sense.
We should be able to configure each salaried employee with their own salary,
but it is irrelevant to other types of employee, who don't have a salary.

Because abstract classes aren't meant to be instantiated directly, their initializers are considered *private*, and so can be freely overridden and have their signatures changed in each subclass.
This offers flexibility in specifying how subclasses are created,
and in fact it is often the case that different subclasses of the same abstract class will have different initializer signatures.
However, subclass initializers should always call the initializer of their superclass!

It turns out that Python allows us to change the signature of *any* method we override, not just `__init__`.
However, as we'll discuss in the next section, in this course we'll use inheritance to define *interfaces* that your subclasses should implement.
Because a function signature is a crucial part of its interface, you should not do this for uses of inheritance in this course.

<!--
We are still overriding, even if only the method name is the same.
This little example demonstrates this:

```Python
class Top:
    def holler(self) -> None:
        print('Hey!!')

class Bottom(Top):
    def holler(self, howmuch: int) -> None:
        print('Hey!!' * howmuch)

>>> t = Top()
>>> t.holler()
Hey!!
>>> b = Bottom()
>>> # We can call the overriding method:
>>> b.holler(3)
Hey!!Hey!!Hey!!
>>> # But we can't call the overridden method:
>>> b.holler()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: holler() missing 1 required positional argument: 'howmuch'
```

The name `holler` itself has been overridden, not the signature as a whole.
In other words, when Python is doing method resolution it is simply looking for the method name, not the whole signature. -->

[^1]: We put an underscore at the end of the attribute `id_` in order to distinguish it from the built-in function `id`.
[^2]: Python has a much more powerful mechanism for accessing the superclass without naming it directly.
    It involves the built-in `super` function, but this is beyond the scope of this course.
[^3]: In many other languages, instance attributes are inherited.
