# Inheritance

Course: [Python](https://www.devtutorials.ai/learn/python) · Section: Object Oriented Programming
Source: https://www.devtutorials.ai/learn/python/oop/inheritance

> Parent and child classes, the five types of inheritance, inheriting and overriding, super(), and isinstance() and issubclass().

Everyone at a school is a person. A person has a name and an age. A student is a person with marks and a class. A teacher is a person with a subject and a salary. A principal is a teacher with extra responsibilities.

You could copy the code for a name and age into each class. Instead, write that shared code once in `Person` and let the other classes build on it. **Inheritance** is how one class reuses and extends another class.

```python runnable title="Said once, used by both"
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        return f"I am {self.name}, {self.age} years old"

class Student(Person):
    pass

class Teacher(Person):
    pass

print(Student("Asha", 13).introduce())
print(Teacher("Meera", 34).introduce())
```

`Student` and `Teacher` do not define any methods of their own here. They can still use `Person`'s `__init__` and `introduce()` because they inherit them.

Inheritance lets one class reuse the attributes and methods of another class, then add or change behaviour.

## What You Will Learn

In this lesson, you will learn:

- What inheritance is, and the words used to describe it
- The five shapes inheritance can take
- How a child class adds to and reuses what it inherited
- How `super()` calls a method in the inheritance search order
- How to check an object's type and how classes relate

## Inheritance

### What Is Inheritance?

**Inheritance** lets you define one class using another class as a starting point. The new class can use the parent's methods and attributes, then add its own or replace a method with a different version.

### Parent and Child Classes

The class being used as a starting point is the **parent**, also called the base class or superclass. The new class is the **child**, also called the derived class or subclass.

```text
class Child(Parent):
```

```python runnable title="A child adding something of its own"
class Person:
    def __init__(self, name):
        self.name = name

    def introduce(self):
        return f"I am {self.name}"

class Student(Person):
    def study(self):
        return f"{self.name} is studying"

asha = Student("Asha")

print(asha.introduce())
print(asha.study())
```

`Student` defines one method, `study()`. It can also use `introduce()` from `Person`, so the object has both methods.

### Benefits of Inheritance

Without inheritance, the same behaviour may need to be written in more than one class:

```python runnable title="The same code written twice"
class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        return f"I am {self.name}, {self.age} years old"

class Teacher:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        return f"I am {self.name}, {self.age} years old"

print(Student("Asha", 13).introduce())
print(Teacher("Meera", 34).introduce())
```

Both examples work. If you change how `introduce()` is written, you must update both classes. With more classes, it is easy to miss one.

<div className="tscroll">

| Benefit | What it means |
|---|---|
| One place to change | shared behaviour lives in the parent |
| Less to read | a child shows only what is different |
| A shared type | code that accepts a `Person` can also accept a `Student` or `Teacher` |
| Easy to extend | a new kind of person can reuse the code that already exists |

</div>

### Code Reusability

When deciding whether inheritance fits, ask: **is one thing a kind of the other?** A student is a person, so `Student` can inherit from `Person`. A student has marks, so marks belong as an attribute, not as a parent class.

```python runnable title="Three kinds of person, one shared base"
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        return f"I am {self.name}, {self.age}"

class Student(Person):
    def role(self):
        return "studies here"

class Teacher(Person):
    def role(self):
        return "teaches here"

class Visitor(Person):
    def role(self):
        return "is visiting"

for person in [Student("Asha", 13), Teacher("Meera", 34), Visitor("Ravi", 40)]:
    print(person.introduce(), "-", person.role())
```

The loop can call `introduce()` on all three objects because each is a kind of `Person`. Each class also supplies its own `role()` method.

Use inheritance for an "is a" relationship, not a "has a" or "uses a" relationship. A `Car` is a `Vehicle`, so inheritance fits. A `Car` has an `Engine`, so the engine should be stored as an attribute instead of making `Car` inherit from `Engine`.

## Types of Inheritance

### Single Inheritance

Single inheritance has one parent class and one child class. It is the simplest and most common form.

```python runnable title="Single inheritance"
class Person:
    def __init__(self, name):
        self.name = name

class Student(Person):
    def study(self):
        return f"{self.name} is studying"

print(Student("Asha").study())
```

### Multiple Inheritance

Multiple inheritance means a class has more than one parent.

```python runnable title="Two parents"
class Teacher:
    def teach(self):
        return "teaching a class"

class Researcher:
    def research(self):
        return "writing a paper"

class Professor(Teacher, Researcher):
    pass

prof = Professor()

print(prof.teach())
print(prof.research())
```

If both parents define a method with the same name, Python searches them in a specific order. In this example, `Teacher` comes first:

```python runnable title="The order in the brackets decides"
class Teacher:
    def describe(self):
        return "I teach"

class Researcher:
    def describe(self):
        return "I research"

class Professor(Teacher, Researcher):
    pass

class Academic(Researcher, Teacher):
    pass

print(Professor().describe())
print(Academic().describe())
```

The two classes have the same parents in a different order, so they find different `describe()` methods first. Python uses a search order called the **method resolution order**, or MRO. You can inspect it:

```python runnable title="The order Python will search"
class Teacher:
    def describe(self):
        return "I teach"

class Researcher:
    def describe(self):
        return "I research"

class Professor(Teacher, Researcher):
    pass

print([cls.__name__ for cls in Professor.__mro__])
```

For this example, Python searches `Professor`, then `Teacher`, then `Researcher`, then `object`. It uses the first class in that order that defines the method.

Multiple inheritance can make the search harder to follow, so use it only when a class genuinely needs to inherit from more than one parent.

### Multilevel Inheritance

Multilevel inheritance is a chain: one child inherits from a class that is itself a child.

```python runnable title="Three levels"
class Person:
    def __init__(self, name):
        self.name = name

    def introduce(self):
        return f"I am {self.name}"

class Teacher(Person):
    def teach(self):
        return f"{self.name} teaches"

class Principal(Teacher):
    def manage(self):
        return f"{self.name} runs the school"

head = Principal("Meera")

print(head.introduce())
print(head.teach())
print(head.manage())
```

`Principal` inherits from `Teacher`, and `Teacher` inherits from `Person`. That is why `head` can use all three methods.

### Hierarchical Inheritance

Hierarchical inheritance has one parent and several child classes. Each child can reuse the same parent behaviour.

```python runnable title="Several children, one parent"
class Person:
    def __init__(self, name):
        self.name = name

    def introduce(self):
        return f"I am {self.name}"

class Student(Person):
    pass

class Teacher(Person):
    pass

class Cleaner(Person):
    pass

for person in [Student("Asha"), Teacher("Meera"), Cleaner("Ravi")]:
    print(type(person).__name__, "->", person.introduce())
```

### Hybrid Inheritance

Hybrid inheritance combines more than one of the shapes above. Here, `Student` and `Teacher` both inherit from `Person`, and `TeachingAssistant` inherits from both:

```text
Person
  ├─ Student ─┐
  └─ Teacher ─┴─ TeachingAssistant
```

```python runnable title="A hierarchy that joins back together"
class Person:
    def __init__(self, name):
        self.name = name

class Student(Person):
    def study(self):
        return f"{self.name} studies"

class Teacher(Person):
    def teach(self):
        return f"{self.name} teaches"

class TeachingAssistant(Student, Teacher):
    pass

ta = TeachingAssistant("Asha")

print(ta.study())
print(ta.teach())
print([cls.__name__ for cls in TeachingAssistant.__mro__])
```

`Person` appears once in the search order even though both parents inherit from it. This is called the diamond shape. Python's method resolution order decides where it looks for methods.

<div className="tscroll">

| Type | Shape | Example |
|---|---|---|
| Single | one parent, one child | `Student(Person)` |
| Multiple | two or more parents | `Professor(Teacher, Researcher)` |
| Multilevel | a chain | `Principal(Teacher(Person))` |
| Hierarchical | one parent, several children | `Student`, `Teacher`, `Cleaner` from `Person` |
| Hybrid | a mixture | `TeachingAssistant(Student, Teacher)` |

</div>

## Working with Inheritance

### Inheriting Attributes

A child class can use class attributes from its parent. When the parent's constructor runs, it can also set instance attributes on the child object.

```python runnable title="The parent's constructor does the work"
class Person:
    species = "human"

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

class Student(Person):
    pass

asha = Student("Asha", 13)

print(asha.name, asha.age, asha.species)
print(vars(asha))
```

`Student` does not define its own constructor, so Python finds and uses the inherited `Person.__init__()`. The `species` class attribute is also available through the student object.

### Inheriting Methods

```python runnable title="Methods come down the chain too"
class Person:
    def __init__(self, name):
        self.name = name

    def introduce(self):
        return f"I am {self.name}"

    def greet(self, other):
        return f"{self.name} greets {other.name}"

class Student(Person):
    pass

asha = Student("Asha")
ravi = Student("Ravi")

print(asha.introduce())
print(asha.greet(ravi))
```

### Calling Parent Methods

In a simple parent-child relationship, when a child defines its own constructor but also needs the parent's setup, `super()` calls the parent's constructor.

```python runnable title="super() runs the parent's constructor"
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Student(Person):
    def __init__(self, name, age, marks):
        super().__init__(name, age)
        self.marks = marks

asha = Student("Asha", 13, 78)

print(vars(asha))
```

`super().__init__(name, age)` lets `Person` set the name and age. Then `Student` adds the marks.

If the child does not call the parent's constructor, the attributes that constructor normally sets will be missing:

```python runnable expect-error title="Forgetting to call the parent's constructor"
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Student(Person):
    def __init__(self, name, age, marks):
        self.marks = marks

asha = Student("Asha", 13, 78)

print(asha.marks)
print(asha.name)
```

`marks` was set, but `name` was not, because `Person.__init__()` never ran.

`super()` can also call a method other than the constructor:

```python runnable title="Adding to the parent's answer"
class Person:
    def __init__(self, name):
        self.name = name

    def introduce(self):
        return f"I am {self.name}"

class Student(Person):
    def __init__(self, name, marks):
        super().__init__(name)
        self.marks = marks

    def introduce(self):
        return super().introduce() + f", and I scored {self.marks}"

print(Student("Asha", 78).introduce())
```

The child calls the parent's `introduce()` and adds the student's marks to the returned sentence. This reuses the parent's work instead of copying the whole method.

### Extending Existing Classes

A child class can also add behaviour to a built-in class.

```python runnable title="Building on a built-in"
class MarkSheet(list):
    def average(self):
        return round(sum(self) / len(self), 1)

    def passed(self):
        return [mark for mark in self if mark >= 35]

marks = MarkSheet([78, 20, 85, 60])

print(marks)
print(len(marks))
print(marks.average())
print(marks.passed())

marks.append(92)
print(marks)
```

`MarkSheet` is a list with two extra methods. It also keeps the usual list operations, such as `len()` and `append()`. The `average()` example uses a list with at least one mark.

In a simple parent-child relationship, `super()` lets each class handle its own part. The parent sets the values it knows about, and the child adds its own values without repeating the parent's setup.

## Relationship Functions

### `isinstance()`

`isinstance(object, Class)` checks whether an object was made from that class or from a child class of it.

```python runnable title="Asking what an object is"
class Person:
    pass

class Student(Person):
    pass

asha = Student()

print(isinstance(asha, Student))
print(isinstance(asha, Person))
print(isinstance(asha, object))
```

A `Student` is also a `Person`, so both checks are `True`. Every Python class also inherits from the built-in `object` class.

`type()` answers a different question: it gives the exact class used to make the object.

```python runnable title="type() is exact, isinstance() is not"
class Person:
    pass

class Student(Person):
    pass

asha = Student()

print(type(asha) is Student)
print(type(asha) is Person)
print(isinstance(asha, Person))
```

Use `isinstance()` when you want to accept an object of a class or any of its child classes. `type(x) is C` only accepts objects made directly from exactly `C`, not its child classes.

You can give `isinstance()` a tuple of classes to check more than one kind at once:

```python runnable title="Checking against several classes"
class Person:
    pass

class Student(Person):
    pass

class Teacher(Person):
    pass

for person in [Student(), Teacher()]:
    print(type(person).__name__, isinstance(person, (Student, Teacher)))
```

### `issubclass()`

`issubclass()` checks how two classes are related. It asks whether the first class is the same as, or a child of, the second class.

```python runnable title="Asking how classes relate"
class Person:
    pass

class Student(Person):
    pass

print(issubclass(Student, Person))
print(issubclass(Person, Student))
print(issubclass(Student, object))
print(issubclass(Student, Student))
```

A class counts as its own subclass, so the last check is also `True`.

### Object Relationship Checking

These checks are useful when a function needs to handle different kinds of objects in different ways.

```python runnable title="One function, several kinds"
class Person:
    def __init__(self, name):
        self.name = name

class Student(Person):
    def __init__(self, name, marks):
        super().__init__(name)
        self.marks = marks

class Teacher(Person):
    def __init__(self, name, subject):
        super().__init__(name)
        self.subject = subject

def describe(person):
    if not isinstance(person, Person):
        return "not a person at all"
    if isinstance(person, Student):
        return f"{person.name} is a student scoring {person.marks}"
    if isinstance(person, Teacher):
        return f"{person.name} teaches {person.subject}"
    return f"{person.name} is some other kind of person"

print(describe(Student("Asha", 78)))
print(describe(Teacher("Meera", "Maths")))
print(describe(Person("Ravi")))
print(describe("just some text"))
```

The order matters: every `Student` and `Teacher` is also a `Person`, so check those child classes before the general `Person` case. This is like putting more specific conditions before a broader one in an `if` chain.

Every class also has information about its direct parent classes and its full method search order:

```python runnable title="Looking at the family tree"
class Person:
    pass

class Teacher(Person):
    pass

class Principal(Teacher):
    pass

print(Principal.__bases__)
print([cls.__name__ for cls in Principal.__mro__])
```

## Common mistakes

Each row shows a mistake, what appears, why it happens, and how to fix it.

<div className="tscroll">

| Mistake | What you see | Why it happens | Fix |
|---|---|---|---|
| A child constructor that never calls the parent's | `AttributeError` for an attribute the parent sets | The parent's `__init__` never ran | Call `super().__init__(...)` |
| Calling `super().__init__()` with the wrong arguments | `TypeError: missing 1 required positional argument` | The parent still needs its own values | Pass the values the parent expects |
| Using inheritance for "has a" | A class with methods that do not belong to it | Inheritance means "is a" | Store the other object as an attribute |
| `type(x) is Person` to accept subclasses | `False` for a `Student` | `type()` checks only the exact class | Use `isinstance(x, Person)` |
| Checking the widest class first | Every object takes the first branch | A `Student` is also a `Person` | Check child classes first |
| Expecting the second parent's method to win | The first parent's version runs | The MRO checks the first parent first in this example | Check `__mro__` |
| Overriding a method and forgetting the parent's work | Part of the original behaviour is missing | The child replaced the method instead of extending it | Call `super().method()` inside |
| Giving a child and parent attribute the same name | The child's assignment replaces that value on the object | Both assignments use the same attribute name | Use a distinct name unless replacing the value is intended |
| Deep inheritance chains | It is hard to find where a method comes from | Many levels make the search harder to follow | Prefer shallow trees and attributes |

</div>

## What to Remember

- **Inheritance lets one class reuse and extend another class**, without copying its methods.
- The class being inherited from is the parent; the new class is the child.
- Use inheritance for **"is a"** relationships. Use an attribute for "has a" relationships.
- The five shapes are single, multiple, multilevel, hierarchical and hybrid.
- With multiple parents, Python follows the method resolution order to find a method. `__mro__` shows that order.
- A child with no `__init__` can use the parent's. **A child that defines its own constructor must call `super().__init__(...)`** if it needs the parent's setup.
- In a simple parent-child relationship, `super()` lets a child add to the parent's method instead of copying it.
- `isinstance(obj, Class)` accepts instances of that class and its child classes. `type(x) is C` checks for the exact class.
- `issubclass(A, B)` asks whether one class is the same as, or inherits from, another.
- When checking object types in a chain, **check the most specific classes first**.

Next: **Polymorphism**
