Python with AI Tutorial · Chapter 33 of 48
Python inheritance lets one class reuse the attributes and methods of another. The existing class is the parent (or base) class, the new one is the child (or derived) class. A child inherits everything from its parent, can add new behaviour and can override methods it wants to change, while super() calls the parent version when needed.
Creating a parent and child class
To inherit, put the parent class name in parentheses after the child class name. The child gets every method of the parent without rewriting a single line.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def annual_cost(self):
return self.salary * 12
class Manager(Employee):
pass
m = Manager("Priya", 90000)
print(m.name, m.annual_cost())
print(isinstance(m, Employee))
Output: Priya 1080000 True
Manager has no code of its own yet, but it can already be created with a name and salary and can call annual_cost(). isinstance() confirms that a Manager is also an Employee. The pass keyword is only a placeholder; you will replace it as soon as the child needs something of its own.
Adding attributes with super().__init__()
Most child classes need extra data. Define your own __init__, call super().__init__() to let the parent set up its part, then add the new attributes.
class Manager(Employee):
def __init__(self, name, salary, team_size):
super().__init__(name, salary)
self.team_size = team_size
m = Manager("Priya", 90000, 8)
print(m.name, m.salary, m.team_size)
Output: Priya 90000 8
__init__ in the child and forget to call super().__init__(), the parent attributes are never created. You will then get AttributeError: 'Manager' object has no attribute 'name' the first time a parent method runs.Overriding a method
A child can replace a parent method simply by defining a method with the same name. Python always looks in the child class first.
class Contractor(Employee):
def __init__(self, name, hourly_rate, hours_per_month):
super().__init__(name, salary=hourly_rate * hours_per_month)
self.hourly_rate = hourly_rate
def annual_cost(self):
# contractors work 11 months a year in this company
return self.salary * 11
staff = [Employee("Arjun", 50000), Manager("Priya", 90000, 8), Contractor("Lee", 800, 160)]
for person in staff:
print(f"{person.name:<6} {type(person).__name__:<11} {person.annual_cost():>10,}")
Output: Arjun Employee 600,000 Priya Manager 1,080,000 Lee Contractor 1,408,000
The loop calls annual_cost() on every object without caring which class it belongs to. Each object runs its own version. This is called polymorphism, and it is the main reason inheritance keeps business code short. In a real payroll script the list might come from a CSV file or a database, and the loop stays exactly the same no matter how many staff types you add later.
Extending a method instead of replacing it
Sometimes you want the parent behaviour plus a little extra. Call super().method() inside the override and build on its result.
class Manager(Employee):
def __init__(self, name, salary, team_size):
super().__init__(name, salary)
self.team_size = team_size
def annual_cost(self):
base = super().annual_cost()
bonus = 2000 * self.team_size
return base + bonus
m = Manager("Priya", 90000, 8)
print(m.annual_cost())
Output: 1096000
If the company later changes how the base salary is annualised, you fix it once in Employee.annual_cost() and every child that calls super() picks up the change automatically.
Ask an assistant to design a class hierarchy for you and explain which methods belong in the parent.
I am modelling staff costs in Python. I have permanent employees (monthly salary), managers (salary plus 2000 per team member per year) and contractors (hourly rate x hours, paid 11 months a year). Design a parent class Employee and child classes Manager and Contractor with __init__ using super() and an annual_cost() method on each. Explain which code belongs in the parent and why.
Multiple inheritance and the MRO
A Python class can inherit from more than one parent. Python decides which parent to search first using the Method Resolution Order (MRO), which you can inspect with ClassName.__mro__.
class Exportable:
def to_dict(self):
return self.__dict__
class Auditable:
def audit_line(self):
return f"AUDIT: {self.name}"
class Manager(Employee, Exportable, Auditable):
def __init__(self, name, salary, team_size):
super().__init__(name, salary)
self.team_size = team_size
m = Manager("Priya", 90000, 8)
print(m.to_dict())
print(m.audit_line())
print([cls.__name__ for cls in Manager.__mro__])
Output:
{'name': 'Priya', 'salary': 90000, 'team_size': 8}
AUDIT: Priya
['Manager', 'Employee', 'Exportable', 'Auditable', 'object']
Small classes like Exportable and Auditable that add one capability are called mixins. Keep them free of __init__ so they can be combined safely. When two parents define the same method name, the one listed first wins because it appears earlier in the MRO.
Useful built-ins for inheritance
These functions and dunder attributes help you inspect and control class relationships.
| Name | What it does | Example |
|---|---|---|
isinstance(obj, Cls) |
True if obj is Cls or a subclass of it | isinstance(m, Employee) → True |
issubclass(A, B) |
True if class A inherits from B | issubclass(Manager, Employee) → True |
super() |
Proxy to the next class in the MRO | super().__init__(name, salary) |
Cls.__mro__ |
Tuple of classes in lookup order | Manager.__mro__ |
Cls.__bases__ |
Direct parent classes | Manager.__bases__ |
type(obj).__name__ |
Name of the object’s actual class | 'Contractor' |
__str__ / __repr__ |
Text shown by print() and the REPL; often overridden |
def __str__(self): ... |
Overriding __str__ for readable output
Dunder methods are inherited like any other method. Define __str__ once in the parent and every child prints nicely.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def annual_cost(self):
return self.salary * 12
def __str__(self):
return f"{type(self).__name__}({self.name}, annual={self.annual_cost():,})"
class Intern(Employee):
def annual_cost(self):
return self.salary * 6 # six-month programme
print(Employee("Arjun", 50000))
print(Intern("Sana", 15000))
Output: Employee(Arjun, annual=600,000) Intern(Sana, annual=90,000)
Because type(self).__name__ is evaluated at run time, the parent’s __str__ automatically prints the child’s class name and calls the child’s annual_cost(). You never need to repeat the method in each subclass.
Paste an existing class and ask for a refactor that removes duplicated code through inheritance.
Here are two Python classes that share a lot of code:
class SalesReport:
def __init__(self, title, rows): self.title = title; self.rows = rows
def total(self): return sum(r["Revenue"] for r in self.rows)
def header(self): return f"== {self.title} =="
class InventoryReport:
def __init__(self, title, rows): self.title = title; self.rows = rows
def total(self): return sum(r["Units"] for r in self.rows)
def header(self): return f"== {self.title} =="
Refactor them into a parent class Report and two child classes using inheritance and super(). Keep the behaviour identical and show a short test.
Common mistakes
- Forgetting
super().__init__()in the child constructor, so parent attributes never exist. - Calling
Employee.__init__(self, ...)directly instead ofsuper(); it works, but breaks when the parent class is renamed or when mixins are added. - Overriding a method with a different number of parameters, which makes objects of different classes impossible to loop over uniformly.
- Using inheritance for “has a” relationships. A
Manageris anEmployee, but anInvoiceis not aCustomer; give it acustomerattribute instead. - Building deep hierarchies five or six levels tall. Two or three levels plus mixins is almost always easier to read.
Exercise
Create a parent class Product with name, price and a final_price() method that returns the price unchanged. Create DigitalProduct, which adds 18% tax in final_price() by extending the parent method, and PhysicalProduct, which takes a shipping amount in its constructor and adds it. Print the final price of a 999 digital template and a 1500 physical item with 120 shipping.
Show answer
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def final_price(self):
return self.price
class DigitalProduct(Product):
def final_price(self):
return round(super().final_price() * 1.18, 2)
class PhysicalProduct(Product):
def __init__(self, name, price, shipping):
super().__init__(name, price)
self.shipping = shipping
def final_price(self):
return super().final_price() + self.shipping
print(DigitalProduct("Excel Dashboard", 999).final_price())
print(PhysicalProduct("Desk Planner", 1500, 120).final_price())
Output: 1178.82 1620
Related chapters
- Python Classes and Objects – the basics of
__init__,selfand methods. - Python Iterators and Generators – make your own classes loopable.
- Python Functions – parameters, return values and defaults used in every method.
- Python with AI course hub – all 48 chapters in order.
FAQ
What is inheritance in Python?
Inheritance is a way to create a new class that reuses the attributes and methods of an existing class. The new child class can add features or override methods while keeping everything else from the parent.
What does super() do in Python?
super() returns a proxy to the next class in the method resolution order, usually the parent. Calling super().__init__() or super().method() runs the parent version so you can extend it instead of rewriting it.
Does Python support multiple inheritance?
Yes. A class can list several parents, such as class Manager(Employee, Exportable). Python resolves method lookups using the MRO, which you can inspect with ClassName.__mro__.
Working with spreadsheets too? Ready-made Excel, Google Sheets and Power BI templates are at NextGenTemplates.com.
Chapter 33 of 48 · Python with AI: all 48 chapters



