Inheritance in Python: A Practical Tutorial with Examples

Learn how Python classes inherit state and behaviour through runnable examples. Trace super(), overrides, MRO, common mistakes, and three focused exercises.

KnowledgeGate Team

Exam prep & CS education

Updated 19 Sep 20265 min read

You can write a Python class, but reusing its state and behaviour in another class can still feel unclear. Copying methods works briefly, then creates two versions to maintain. Inheritance lets a child class reuse a base class, while overriding and super() control what changes. Method resolution order decides which implementation Python calls when more than one parent is involved.

What inheritance means in Python, and when it fits

Inheritance creates a relationship in which a child class receives accessible attributes and methods from a parent class. The child can use that behaviour unchanged, extend it, or replace selected methods. When you access a method, Python first checks the object's class, then follows its parent classes in method resolution order (MRO).

Use inheritance when the sentence “a child is a parent” makes sense. A PythonCourse is a Course, so inheritance fits. A Course has a Lesson, so that relationship should normally use composition: store a lesson object inside the course. This distinction prevents forced class hierarchies. Inheritance is one useful technique within the broader foundation covered by Coding & Skills courses, but it is not the answer to every reuse problem.

Your first base class and child class

python
class Device:
    def __init__(self, label):
        self.label = label

    def identify(self):
        return f"Device: {self.label}"


class Laptop(Device):
    pass


item = Laptop("Lab-07")
print(item.identify())
print(isinstance(item, Device))
Code
Device: Lab-07
True

The syntax class Laptop(Device): declares Laptop as a child of Device. Laptop defines neither __init__ nor identify, so Python finds both on Device. The inherited initialiser stores "Lab-07" on the new object, and the inherited method reads it. The object remains a Laptop, while isinstance also recognises it as a Device. The pass statement is valid because the class body cannot be empty; here it says that the child adds no behaviour yet. That means the child starts useful without duplicating code. You can add Laptop methods later, while shared Device behaviour remains in one place rather than becoming two copies that need separate maintenance.

Worked example: extend a parent with super() and override a method

Course owns state shared by every course. PythonCourse adds a project count and specialises the progress message.

python
class Course:
    def __init__(self, title, lessons):
        self.title = title
        self.lessons = lessons

    def progress(self, completed):
        return f"{completed}/{self.lessons} lessons"


class PythonCourse(Course):
    def __init__(self, title, lessons, projects):
        super().__init__(title, lessons)
        self.projects = projects

    def progress(self, completed):
        base = super().progress(completed)
        percent = completed / self.lessons * 100
        return f"{base} ({percent:.0f}%), projects: {self.projects}"


course = PythonCourse("Python Foundations", 24, 3)
print(course.title)
print(course.progress(18))
print(isinstance(course, Course))
Code
Python Foundations
18/24 lessons (75%), projects: 3
True

Follow the values step by step. super().__init__ continues lookup to Course.__init__, which stores title="Python Foundations" and lessons=24. The child then stores projects=3.

Calling course.progress(18) selects the child's override. Its super().progress(18) call obtains the parent string 18/24 lessons. The calculation is 18 / 24 * 100 = 0.75 * 100 = 75. The :.0f format displays that value with no decimal places. The child combines the inherited result with its own percentage and project data. If you want structured practice after retyping the example, the Python programming course is an optional next step.

Class diagram: PythonCourse inherits from Course; super().progress(18) returns 18/24 lessons before adding 75% and projects: 3.

The five inheritance shapes you should recognise

Python supports five commonly named inheritance shapes:

Shape

Exact relationship

What it demonstrates

Single

PythonCourse -> Course

One child and one direct parent

Multilevel

PythonCourse -> Course -> Content

Inheritance across multiple levels

Hierarchical

PythonCourse and SQLCourse inherit Course

Several children share one parent

Multiple

PythonCourse inherits Course and Trackable

One child has multiple direct parents

Hybrid

RecordedPythonCourse inherits PythonCourse, while PythonCourse also inherits Trackable

Two or more shapes are combined

These labels describe structures, not reasons to build them. Keep the “is-a” test and a clear lookup path as the design criteria. Every user-defined class ultimately inherits from object, but that fact does not turn object -> Course -> PythonCourse into an intentional multilevel design.

Method overriding, multiple inheritance, and MRO

Overriding means defining a child method with the same name as a parent method. PythonCourse.progress wins because lookup checks the child first. Its super() call then deliberately continues after PythonCourse in the MRO.

python
class Logger:
    def describe(self):
        return "logger"


class Repository:
    def describe(self):
        return "repository"


class CachedRepository(Logger, Repository):
    pass


repo = CachedRepository()
print(repo.describe())
print([cls.__name__ for cls in CachedRepository.__mro__])
Code
logger
['CachedRepository', 'Logger', 'Repository', 'object']

The parent order in class CachedRepository(Logger, Repository) produces that MRO, so Logger.describe is the first match. Reversing the parents makes Repository.describe win. When several classes cooperate, design their methods to call super() with compatible arguments. Directly naming a parent inside reusable multiple-inheritance code can skip another class in the MRO and break that cooperation.

MRO path CachedRepository to Logger to Repository to object, where Logger.describe() is the first match and returns the string logger.

Common inheritance errors and the fix for each

Each common failure has a direct repair:

  • Missing parent initialisation: omitting super().__init__ leaves title and lessons unset, so course.progress(18) raises AttributeError. Call super().__init__(title, lessons).

  • Wrong initialiser arguments: passing a missing or extra argument to the parent raises TypeError. Match the parent's method signature.

  • Accidental recursion: return self.progress(completed) inside the override selects the same override repeatedly and ends in RecursionError. Call super().progress(completed).

  • Accessing __lessons directly: a double-leading underscore triggers name mangling. If subclass access is intentional, expose a property or use a documented _lessons convention.

  • Using inheritance for “has-a”: making Lesson a parent of Course gives the model the wrong meaning. Store a contained Lesson object instead.

Read the exception from the first missing attribute or bad call, then repair the hierarchy instead of adding unrelated defaults. A small failing example usually exposes the design error faster.

How interviews and exams test inheritance, plus three exercises

Stable question forms ask you to predict output from an override, identify the first method chosen by an MRO, or debug a missing parent initialiser. Practise those skills without assuming any particular exam pattern. Before running code, write the expected lookup path beside each call. This habit separates method selection from method execution and makes MRO questions much easier to debug under pressure.

  1. Create VideoCourse(Course) with lessons=16 and quality="1080p". Make progress(12) return 12/16 lessons (75%), quality: 1080p. Check the arithmetic: 12 / 16 * 100 = 75.

  2. Predict both output lines from CachedRepository. Reverse its parents and confirm that the first line becomes repository.

  3. Remove super().__init__ from PythonCourse, observe the AttributeError, restore the call, and reproduce 18/24 lessons (75%), projects: 3.

For more code tracing, compare control flow in the sorting algorithms comparison and the dynamic programming worked example. Once the OOP basics are comfortable, the DSA Using Python course offers a structured move into problem-solving practice.

Inheritance in Python: the short version and next step

Use inheritance for a genuine is-a relationship. Initialise shared state through super(), override only what differs, inspect ClassName.__mro__ when multiple parents are involved, and choose composition for has-a relationships. Now retype the Course example with lessons=30, completed=21, and projects=4. Since 21 / 30 * 100 = 70, the message must be 21/30 lessons (70%), projects: 4. Use the printed result as your checkpoint. Verify it, then attempt the three exercises.