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

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
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))Device: Lab-07
TrueThe 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.
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))Python Foundations
18/24 lessons (75%), projects: 3
TrueFollow 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.

The five inheritance shapes you should recognise
Python supports five commonly named inheritance shapes:
Shape | Exact relationship | What it demonstrates |
|---|---|---|
Single |
| One child and one direct parent |
Multilevel |
| Inheritance across multiple levels |
Hierarchical |
| Several children share one parent |
Multiple |
| One child has multiple direct parents |
Hybrid |
| 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.
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__])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.

Common inheritance errors and the fix for each
Each common failure has a direct repair:
Missing parent initialisation: omitting
super().__init__leavestitleandlessonsunset, socourse.progress(18)raisesAttributeError. Callsuper().__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 inRecursionError. Callsuper().progress(completed).Accessing
__lessonsdirectly: a double-leading underscore triggers name mangling. If subclass access is intentional, expose a property or use a documented_lessonsconvention.Using inheritance for “has-a”: making
Lessona parent ofCoursegives the model the wrong meaning. Store a containedLessonobject 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.
Create
VideoCourse(Course)withlessons=16andquality="1080p". Makeprogress(12)return12/16 lessons (75%), quality: 1080p. Check the arithmetic:12 / 16 * 100 = 75.Predict both output lines from
CachedRepository. Reverse its parents and confirm that the first line becomesrepository.Remove
super().__init__fromPythonCourse, observe theAttributeError, restore the call, and reproduce18/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.
Keep learning

Decorators in Python: @ Syntax, Wrappers and Worked Examples
Understand when decoration happens, what a wrapper receives, and how return values travel back. Runnable examples cover parameters, stacking and built-ins.

Exceptions in Python: Handle Errors with try, except and finally
Learn how Python exceptions change control flow, how to catch expected failures without hiding bugs, and when to use else, finally, raise, and custom exceptions.

Context Managers in Python: with, __enter__, __exit__ and contextlib
Learn how Python's with statement guarantees cleanup, then build and trace class-based and generator-based context managers with runnable examples.

Python Arguments and Scope: Parameters, LEGB, and Worked Examples
Trace how Python binds arguments, handles mutation and rebinding, and resolves names through LEGB. Runnable examples turn scope rules into predictable output.