# Inheritance and super: Similar Classes Without Duplicated Code<no value>

## In this lesson

This lesson is built as one story, in the same order as the slides:

1. **Chapter 1** — why inheritance: the same code twice, and how to inherit instead of copying
2. **Chapter 2** — overriding and `super()`: inheritance, but not exactly — changing only part of the behavior

At the end of the lesson — practice, and a **mini project** that connects both chapters.

## Chapter 1: Why Inheritance — Parent, Child and the Is-a-Kind-of Relationship

### What will we learn?

- **Why** inheritance is needed: the problem of almost identical Classes.
- **Parent Class** (Base Class) and **Child Class** (Subclass).
- **The syntax** of inheritance in Python.
- **What is passed on** through inheritance.
- **The is-a test**: when inheritance suits and when it does not.

### Why do we need this?

In the lesson on classes and objects we asked: what happens when you add a "savings account" that is similar to a regular account, but with interest? In the procedural version, every function filled up with `if account["type"] == ...`. Let's see the same problem in OOP, **without** inheritance:

```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * 1.25

    def describe(self):
        return self.name + " earns " + str(self.salary)

class Manager:
    def __init__(self, name, salary, team_size):
        self.name = name
        self.salary = salary
        self.team_size = team_size

    def monthly_cost(self):
        return self.salary * 1.25

    def describe(self):
        return self.name + " earns " + str(self.salary)

    def team_report(self):
        return self.name + " manages " + str(self.team_size) + " people"
```

`Manager` **copies** almost all of `Employee`: `name`, `salary`, `monthly_cost`, `describe`. What happens when the employer cost changes from 1.25 to 1.3? You have to fix it **in two places**, and if there are also `Intern`, `Contractor` and `Director`, in five. **This is a violation of DRY at the level of Classes.**

And one more thing: **a manager is an employee.** Everything that is true for an employee is also true for a manager, and in addition the manager has more things. The code should reflect this relationship.

### Explanation from the basics

#### Inheritance

**Inheritance** = a mechanism in which a new Class **automatically receives** all the Attributes and methods of an existing Class, and can **add** or **change** parts.

- **Parent Class** (Base, Superclass) — the general Class. `Employee`.
- **Child Class** (Derived, Subclass) — the specific Class, which **inherits**. `Manager`.

{{< box "analogy" "Analogy: a car production template — the sports version" >}}
We have a production template of a "car" (the Parent): 4 wheels, an engine, steering, braking. Now we want a "sports car". **We don't design a car from scratch.** We take the existing template, and **add** a spoiler and **change** the engine. Every sports car **is a car** (it drives, it brakes), but not every car is a sports car.
{{< /box >}}

#### The "is a kind of" relationship (is-a)

Inheritance suits **only** when you can honestly say: **"the Child is a kind of the Parent"**.

| Sentence | Inheritance? |
|---|---|
| A manager **is a kind of** employee | ✓ `Manager(Employee)` |
| A dog **is a kind of** animal | ✓ `Dog(Animal)` |
| A savings account **is a kind of** bank account | ✓ `SavingsAccount(BankAccount)` |
| A car **is a kind of** engine? | ✗ A car **has** an engine (has-a), and that is **Composition**, the OOP design topic |
| An order **is a kind of** customer? | ✗ An order **has** a customer |

#### What is passed on through inheritance?

The Child receives **everything** from the Parent:

- The `__init__` (if the Child does not define an `__init__` of its own).
- **All the methods**.
- **The Class Attributes**.

And the Child can:

- **Add** new methods and Attributes.
- **Change** (Override) existing methods. We will learn it in the next chapter, together with `super()`.

#### Lookup in layers — again

When you write `manager.describe()`, Python looks: **(1)** on the object, **(2)** in its Class (`Manager`), **(3)** in the Parent (`Employee`)... and stops at the first place where it found it. This is the same idea of a layered lookup that we saw in LEGB and in the lookup order of Attributes.

### Syntax

```python
class Parent:
    def __init__(self, x):
        self.x = x

    def shared_method(self):
        return ...

class Child(Parent):             # Child inherits from Parent
    def extra_method(self):      # new method, only in Child
        return ...

c = Child(5)                     # uses Parent.__init__ (Child has none of its own)
c.shared_method()                # inherited
c.extra_method()                 # Child's own
```

- The Parent's name comes **in brackets** after the Child's name.
- A Child without any addition: `class Child(Parent): pass` is a valid Class that is identical to the Parent.

### First code example

`week-3/employees.py`:

```python
class Employee:
    EMPLOYER_COST_RATE = 1.25

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * Employee.EMPLOYER_COST_RATE

    def describe(self):
        return self.name + " earns " + str(self.salary)

class Manager(Employee):
    def team_report(self, team_size):
        return self.name + " manages " + str(team_size) + " people"

dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)

print(dana.describe())
print(avi.describe())
print(avi.monthly_cost())
print(avi.team_report(5))
```

**Output:**

```text
Dana earns 12000
Avi earns 20000
25000.0
Avi manages 5 people
```

### Breaking down the code line by line

| Code | Explanation |
|---|---|
| `class Manager(Employee):` | `Manager` **inherits** from `Employee`. |
| `Manager("Avi", 20000)` | `Manager` has no `__init__` of its own, so Python finds the one of `Employee` and uses it. Avi gets a `name` and a `salary`. |
| `avi.describe()` | There is no `describe` in `Manager`. Python goes up to `Employee` and finds it. **`self` is still Avi.** |
| `avi.monthly_cost()` | The same, and `EMPLOYER_COST_RATE` is also found through the Parent. |
| `avi.team_report(5)` | A method that exists **only** in `Manager`. |
| `dana.team_report(...)` | Would throw an `AttributeError`: a regular employee does **not** inherit from the manager. Inheritance goes **from top to bottom** only. |

**And the fix of 1.25 → 1.3?** You change **one line**, in `Employee`, and all the managers (and in the future, every kind of employee) are updated.

### Another example

**A hierarchy with several children: Animal / Dog / Cat.** `week-3/animals.py`:

```python
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def birthday(self):
        self.age = self.age + 1

    def describe(self):
        return self.name + " (" + str(self.age) + " years old)"

class Dog(Animal):
    def fetch(self):
        return self.name + " brings back the ball"

class Cat(Animal):
    def climb(self):
        return self.name + " climbs the tree"

rex = Dog("Rex", 3)
mitzi = Cat("Mitzi", 5)

rex.birthday()
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())

for animal in [rex, mitzi]:
    print(type(animal).__name__, "->", animal.describe())
```

**Output:**

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
Dog -> Rex (4 years old)
Cat -> Mitzi (5 years old)
```

**Breakdown:**

- **Two children of the same Parent**: `Dog` and `Cat` share everything that is common (`name`, `age`, `birthday`, `describe`), and each one adds what is unique to it.
- `rex.birthday()` — a method **written once** in `Animal`, which changes **rex's** State.
- In the loop, `animal.describe()` works on **every** kind of `Animal`, without checking which kind it is. This is a hint of **Polymorphism**, which we will go deeper into in the next lesson.
- `type(animal).__name__` — the Class's name as text.

```text
         Animal
        /      \
      Dog      Cat
```

### Summary

- **Inheritance** = a Child Class receives all the Attributes and methods of a Parent Class, and can add and change.
- Syntax: `class Child(Parent):`.
- It solves **duplication** between similar Classes (DRY), and reflects the "**is a kind of**" relationship (is-a).
- Python looks for a method in the object's Class, and if it did not find it, goes up to the Parent.
- Inheritance is one-directional: the Parent does not know what was added in the Child.
- **Do not** inherit when the relationship is "**has a**" (has-a). Then you use Composition (the OOP design topic).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

### Official documentation

- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)
- [isinstance()](https://docs.python.org/3/builtins/functions.html#isinstance)

<!-- docs:end -->

## Chapter 2: Overriding and `super()` — Changing Only What Is Different

### What will we learn?

- **`super()`**: how a Child uses the Parent's code.
- **`__init__` in a Child**: adding Attributes without copying the Parent's.
- **Method Overriding**: a Child **replaces** a method of the Parent.
- **Extending**: a Child **adds** to the Parent's method, instead of replacing it completely.

### Why do we need this?

This is where plain inheritance gets stuck: a `Truck` that inherits from a `Vehicle` needs an extra Attribute (`cargo_kg`), meaning an `__init__` of its own. But if we write a new `__init__` in Truck, it **replaces** the one of Vehicle, and then `brand` and `wheels` will not be initialized, unless we **copy** the code. And that is exactly what inheritance came to prevent.

And a second problem: sometimes the Child needs to behave **differently** in the same action. A manager gets a bonus in the cost calculation. A savings account does not allow withdrawing below a minimum. We need a way to **change** a method that came through inheritance.

### Explanation from the basics

#### Method Overriding

If in the Child you define a method **with the same name** as in the Parent, **the Child's version wins** for objects of type Child. This is called an **Override**. Why? Remember the lookup order: Python looks **first** in the object's Class, and only if it did not find it, goes up to the Parent. If the method exists in the Child, Python never even reaches the Parent's version.

#### `super()` — "the Parent's version"

**`super()`** returns a special object through which you call **the Parent's method**, on **the same `self`**. The two main uses:

1. **In `__init__`**: `super().__init__(...)` — "first initialize everything the Parent initializes", and then add what is **unique** to the Child.
2. **In extending a method**: `super().describe()` — "bring me what the Parent returns", and then add to it.

{{< box "analogy" "Analogy: a family recipe" >}}
Grandma (the Parent) bakes a chocolate cake from a recipe. You (the Child) **inherit** the recipe. **Override** = you decide to bake a completely different cake with the same name, "chocolate cake", but vegan, from scratch. **Extending with `super()`** = "do exactly like grandma" (`super().bake()`), **and then** add a frosting you invented. You don't copy grandma's whole recipe. You simply say "like grandma, plus...".
{{< /box >}}

#### Three options for a method in a Child

| Option | What you do | When |
|---|---|---|
| **Inherit** | You write nothing | The Parent's behavior suits |
| **Override** | You write a method with the same name, without `super()` | The behavior is completely different |
| **Extend** | You write a method with the same name, and inside it `super().method()` | "Like the Parent, plus something" |

### Syntax

```python
class Parent:
    def __init__(self, a):
        self.a = a

    def describe(self):
        return "a=" + str(self.a)

    def greet(self):
        return "Hello from Parent"

class Child(Parent):
    def __init__(self, a, b):
        super().__init__(a)          # let Parent set up self.a
        self.b = b                   # then add what is new in Child

    def describe(self):              # EXTEND: reuse Parent's result
        return super().describe() + ", b=" + str(self.b)

    def greet(self):                 # OVERRIDE: replace completely
        return "Hello from Child"
```

**Notice:** in `super().__init__(a)` you do **not** pass `self`. `super()` already "knows" which object it is about.

### First code example

**Solving the Truck problem.** `week-3/vehicles_super.py`:

```python
class Vehicle:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels

    def describe(self):
        return self.brand + " with " + str(self.wheels) + " wheels"

class Truck(Vehicle):
    def __init__(self, brand, max_load_kg):
        super().__init__(brand, 6)
        self.max_load_kg = max_load_kg
        self.cargo_kg = 0

    def load(self, kg):
        if self.cargo_kg + kg > self.max_load_kg:
            raise ValueError("over the limit: max " + str(self.max_load_kg) + " kg")
        self.cargo_kg = self.cargo_kg + kg

    def describe(self):
        return super().describe() + ", carrying " + str(self.cargo_kg) + "/" + str(self.max_load_kg) + " kg"

volvo = Truck("Volvo", 1000)
volvo.load(600)
print(volvo.describe())

try:
    volvo.load(500)
except ValueError as error:
    print("Rejected:", error)

print(volvo.brand, volvo.wheels)
```

**Output:**

```text
Volvo with 6 wheels, carrying 600/1000 kg
Rejected: over the limit: max 1000 kg
Volvo 6
```

### Breaking down the code line by line

| Code | Explanation |
|---|---|
| `def __init__(self, brand, max_load_kg):` | Truck has an `__init__` **of its own**, with parameters that suit a truck. |
| `super().__init__(brand, 6)` | "Run Vehicle's `__init__` **on this object**". This creates `self.brand` and `self.wheels`. A truck always has 6 wheels, so the Child decides the value. |
| `self.max_load_kg = ...`, `self.cargo_kg = 0` | **Only** what is new in Truck. Without copying a single line from Vehicle. |
| `def describe(self):` in Truck | **Extending**: the same name as in Vehicle. |
| `super().describe()` | Gets `"Volvo with 6 wheels"` from the Parent, and adds the cargo details to it. |
| `volvo.brand`, `volvo.wheels` | They exist, because `super().__init__` created them. |

**If Vehicle changes tomorrow**, for example adds `self.year`, trucks will get it automatically through `super().__init__`. If we had copied the code, we would need to remember to update Truck too.

### Another example

**A full Override, extending, and `super()` inside business rules.** `week-3/staff.py`:

```python
class Employee:
    EMPLOYER_COST_RATE = 1.25

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * Employee.EMPLOYER_COST_RATE

    def title(self):
        return "Employee"

    def describe(self):
        return self.title() + " " + self.name + " - cost " + str(self.monthly_cost())

class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus

    def title(self):
        return "Manager"

class Intern(Employee):
    def monthly_cost(self):
        return self.salary

    def title(self):
        return "Intern"

staff = [Employee("Dana", 12000), Manager("Avi", 20000, 3000), Intern("Noa", 5000)]

for person in staff:
    print(person.describe())

print("Total monthly cost:", sum(person.monthly_cost() for person in staff))
```

**Output:**

```text
Employee Dana - cost 15000.0
Manager Avi - cost 28000.0
Intern Noa - cost 5000
Total monthly cost: 48000.0
```

**Breakdown:**

- `Manager.monthly_cost` — **extending**: "like a regular employee (`super().monthly_cost()` = 25,000), **plus** a bonus". If the employer cost rate changes, the managers will get the change automatically.
- `Intern.monthly_cost` — **a full Override**: an intern does not carry employer costs, so a completely different calculation, without `super()`.
- `title()` — every Class overrides it.
- **The interesting point:** `describe()` is written **only once**, in `Employee`. It calls `self.title()` and `self.monthly_cost()`. When the object is a Manager, `self.monthly_cost()` reaches **the Manager's version**! The Parent's method automatically uses the Child's overridden versions. This is the basis of **Polymorphism** (the next lesson).
- `Intern` did not define an `__init__`, so it uses the one of `Employee` (regular inheritance).

### Summary

- **Override** = a method in the Child with the same name as in the Parent. The Child's version wins.
- **`super()`** = access to the Parent's version, on the same `self`.
- A Child with an `__init__` of its own **must** call `super().__init__(...)` (without `self`), and then add only what is new.
- **Extending**: `super().method()` + an addition. **A full override**: without `super()`.
- `self.method()` inside an overridden method = recursion. For the Parent's version: `super().method()`.
- A Parent's method that calls `self.x()` will automatically reach the Child's overridden version.
- An overridden method keeps **the same signature** (the same parameters).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

### Official documentation

- [super()](https://docs.python.org/3/builtins/functions.html#super)
- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)

<!-- docs:end -->

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

**What you practice on this page:** inheritance — Parent and Child, the "is a kind of" test, extending `__init__` with `super()`, and overriding a method (Override).

**By the end of the page you will know how to:**
- Write a Child that inherits from a Parent and adds only what is new
- Decide when inheritance suits, using the "is a kind of" test
- Call `super().__init__` so that the Parent initializes its part
- Override a method, and use the Parent's version inside it

### Exercise 1: An employee and a manager

**What you build:** an employee and a manager — where a manager is a kind of employee, so you don't write again what is shared.

**What to write:**
1. A Class named `Employee` with a name and a salary.
2. In `Employee`: `describe` that returns the name and the salary, as in the output.
3. A Class named `Manager` that inherits from `Employee` — without an `__init__` of its own.
4. In `Manager` only: `manage` that receives a team size and returns a sentence as in the output.

**Test code:**

```python
dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))
```

**The desired output:**

```text
Dana: 12000
Avi: 20000
Avi manages 5 people
```

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def describe(self):
        return self.name + ": " + str(self.salary)


class Manager(Employee):
    def manage(self, team_size):
        return self.name + " manages " + str(team_size) + " people"


dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))
```

```text
Dana: 12000
Avi: 20000
Avi manages 5 people
```

`Manager` **inherits** `__init__` and `describe` — without writing them again — and adds only what is new.
{{< /solution >}}

### Exercise 2: is-a — is a kind of

**What you do:** decide — without code — when inheritance suits.

**The task:** for each pair, answer: should the first inherit from the second? Explain with the sentence "X is a kind of Y" — and if the sentence is not true, what the relationship between them actually is.

1. `Dog` and `Animal`
2. `Car` and `Engine`
3. `SavingsAccount` and `BankAccount`
4. `Square` and `Color`

**What to submit:** yes or no for each pair, and one sentence of explanation.

{{< solution >}}
1. **Yes** — a dog **is a kind of** animal.
2. **No** — a car is **not** a kind of engine. A car **has** an engine (has-a). This is a case for composition, not for inheritance.
3. **Yes** — a savings account **is a kind of** bank account.
4. **No** — a square is not a kind of color. Maybe a square **has** a color.

Inheritance only when the sentence "X is a kind of Y" is true.
{{< /solution >}}

### Exercise 3: An animal tree

**What you build:** a small inheritance tree — an animal, and two kinds of animals, each with an action of its own.

**What to write:**
1. A Class named `Animal` with a name and an age, and `describe` that returns the name and the age as in the output.
2. A Class named `Dog` that inherits from `Animal`, with `fetch` that returns the sentence from the output.
3. A Class named `Cat` that inherits from `Animal`, with `climb` that returns the sentence from the output.

**Test code:**

```python
rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())
```

**The desired output:**

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
```

{{< solution >}}
```python
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def describe(self):
        return self.name + " (" + str(self.age) + " years old)"


class Dog(Animal):
    def fetch(self):
        return self.name + " brings back the ball"


class Cat(Animal):
    def climb(self):
        return self.name + " climbs the tree"


rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())
```

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
```
{{< /solution >}}

### Exercise 4: super().__init__

**What you build:** a truck — a vehicle that always has 6 wheels, and also has a maximum cargo weight.

**What to write:**
1. A Class named `Vehicle` with a brand and a number of wheels.
2. A Class named `Truck` that inherits from `Vehicle`. Its `__init__` receives a brand and a maximum weight.
3. Inside the `__init__` of `Truck`: call `super().__init__` with the brand and 6 wheels, and initialize a current weight of 0.
4. `load` — receives kilograms and adds them to the weight. If the maximum is exceeded: a `ValueError` with the message from the output.

**Test code:**

```python
truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
    truck.load(500)
except ValueError as error:
    print("error:", error)
```

**The desired output:**

```text
Volvo 6 600
error: overloaded: max 1000 kg
```

{{< solution >}}
```python
class Vehicle:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels


class Truck(Vehicle):
    def __init__(self, brand, max_load_kg):
        super().__init__(brand, 6)
        self.max_load_kg = max_load_kg
        self.load_kg = 0

    def load(self, kg):
        if self.load_kg + kg > self.max_load_kg:
            raise ValueError("overloaded: max " + str(self.max_load_kg) + " kg")
        self.load_kg = self.load_kg + kg


truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
    truck.load(500)
except ValueError as error:
    print("error:", error)
```

```text
Volvo 6 600
error: overloaded: max 1000 kg
```

`super().__init__(brand, 6)` initializes the Parent's part. After that the Child adds its own Attributes.
{{< /solution >}}

### Exercise 5: Override with super

**What you build:** a manager whose monthly cost is the salary plus a bonus.

**What to write:**
1. A Class named `Employee` with a name and a salary, and `monthly_cost` that returns the salary.
2. A Class named `Manager` that inherits from `Employee`, and also receives a bonus. In its `__init__`, call `super().__init__`.
3. In `Manager`, override `monthly_cost`: the Parent's version, through `super()`, plus the bonus.

**Test code:**

```python
print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())
```

**The desired output:**

```text
12000
23000
```

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus


print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())
```

```text
12000
23000
```

`super().monthly_cost()` calls the Parent's version. If the calculation of the base salary changes, `Manager` will get the change automatically.
{{< /solution >}}

## Mini project: Employees in a company

**Which system you build:** an employee system for a company. The company has regular employees, managers and interns. All of them are employees — they have a name and a salary — but each kind is displayed differently, and a manager also has a bonus. The system displays every employee and calculates the monthly cost of all of them together.

**What you will learn from the project:**
- Build a Parent with everything that is shared, and Children that add only what is different
- Call `super().__init__` from the Child
- Override a method, and extend a method with `super()`
- See how a method written once in the Parent works correctly for every kind

**What to build — step by step:**

**Step 1 — an employee (`Employee`).** `__init__` receives a name and a salary. Three methods:
- `title` — returns the word Employee;
- `monthly_cost` — returns the salary;
- `describe` — returns the name, the role and the monthly cost, as in the output. It uses `title` and `monthly_cost`.

**Step 2 — a manager (`Manager`).** Inherits from `Employee` and also receives a bonus. In `__init__`, call `super().__init__` and save the bonus. Override `title` — the word Manager. Extend `monthly_cost`: the result of `super()` plus the bonus.

**Step 3 — an intern (`Intern`).** Inherits from `Employee` and also receives a number of internship months. In `__init__`, check that the number is between 1 and 12 — otherwise `ValueError` — and then call `super().__init__`. Override `title` — the word Intern. Do **not** write `monthly_cost` and `describe`: they come through inheritance.

**The error message — exactly like this:**

```text
months must be 1-12
```

**Test code:**

```python
staff = [
    Employee("Dana", 12000),
    Manager("Avi", 20000, 3000),
    Intern("Noa", 6000, 6),
]

for person in staff:
    print(person.describe())

try:
    Intern("Tom", 5000, 0)
except ValueError as error:
    print("error:", error)

total = 0
for person in staff:
    total = total + person.monthly_cost()
print("Total monthly cost:", total)
```

**The desired output:**

```text
Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000
```

**By the end of the project you will know how to:**
- Design a hierarchy: what goes up to the Parent and what stays in the Child
- Tell apart three options for a method in a Child: inherit, override, or extend with `super()`
- Understand why `describe` shows every employee **their own** role and cost, even though it was written once

**Bonus:** add to `Manager` a method `manage` that receives a team size and returns a sentence like Avi manages 5 people, and print it only for the managers in the list.

{{< solution >}}
```python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def title(self):
        return "Employee"

    def monthly_cost(self):
        return self.salary

    def describe(self):
        return self.name + " (" + self.title() + "): " + str(self.monthly_cost())


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def title(self):
        return "Manager"

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus


class Intern(Employee):
    def __init__(self, name, salary, months):
        if months < 1 or months > 12:
            raise ValueError("months must be 1-12")
        super().__init__(name, salary)
        self.months = months

    def title(self):
        return "Intern"


staff = [
    Employee("Dana", 12000),
    Manager("Avi", 20000, 3000),
    Intern("Noa", 6000, 6),
]

for person in staff:
    print(person.describe())

try:
    Intern("Tom", 5000, 0)
except ValueError as error:
    print("error:", error)

total = 0
for person in staff:
    total = total + person.monthly_cost()
print("Total monthly cost:", total)
```

```text
Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000
```

- `describe` was written **once** in `Employee`, but every employee gets **their own** `title()` and `monthly_cost()` — because Python looks for the method first in the Child.
- `Manager.monthly_cost` **extends**: `super().monthly_cost()` returns the salary, and the bonus is added. `title` **overrides** completely.
- `Intern` does not write `monthly_cost` — it **inherits** it as it is.

**Bonus:**

```python
    def manage(self, team_size):
        return self.name + " manages " + str(team_size) + " people"
```

(A method inside `Manager`.)

```python
for person in staff:
    if person.title() == "Manager":
        print(person.manage(5))
```

```text
Avi manages 5 people
```
{{< /solution >}}

<!-- exercises:end -->
