Inheritance in Python

By the Syncfusion DevTutorials team · Last updated

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.

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.

class Child(Parent):

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:

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.

BenefitWhat it means
One place to changeshared behaviour lives in the parent
Less to reada child shows only what is different
A shared typecode that accepts a Person can also accept a Student or Teacher
Easy to extenda new kind of person can reuse the code that already exists

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.

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.

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.

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:

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:

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.

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.

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:

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

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.

TypeShapeExample
Singleone parent, one childStudent(Person)
Multipletwo or more parentsProfessor(Teacher, Researcher)
Multilevela chainPrincipal(Teacher(Person))
Hierarchicalone parent, several childrenStudent, Teacher, Cleaner from Person
Hybrida mixtureTeachingAssistant(Student, Teacher)

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.

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

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.

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:

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:

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.

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.

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.

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:

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.

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.

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:

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.

MistakeWhat you seeWhy it happensFix
A child constructor that never calls the parent'sAttributeError for an attribute the parent setsThe parent's __init__ never ranCall super().__init__(...)
Calling super().__init__() with the wrong argumentsTypeError: missing 1 required positional argumentThe parent still needs its own valuesPass the values the parent expects
Using inheritance for "has a"A class with methods that do not belong to itInheritance means "is a"Store the other object as an attribute
type(x) is Person to accept subclassesFalse for a Studenttype() checks only the exact classUse isinstance(x, Person)
Checking the widest class firstEvery object takes the first branchA Student is also a PersonCheck child classes first
Expecting the second parent's method to winThe first parent's version runsThe MRO checks the first parent first in this exampleCheck __mro__
Overriding a method and forgetting the parent's workPart of the original behaviour is missingThe child replaced the method instead of extending itCall super().method() inside
Giving a child and parent attribute the same nameThe child's assignment replaces that value on the objectBoth assignments use the same attribute nameUse a distinct name unless replacing the value is intended
Deep inheritance chainsIt is hard to find where a method comes fromMany levels make the search harder to followPrefer shallow trees and attributes

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