- What is single inheritance — one parent, one child
- Creating parent-child classes — the syntax
- What gets inherited — methods, attributes, and more
- Method overriding — customizing inherited methods
- Using super() — calling parent methods
- Constructors in inheritance — initializing parent and child
What is Single Inheritance?
Single inheritance is the simplest form of inheritance in Python. It's when a child class inherits from exactly one parent class. This creates a clear, linear hierarchy where each class has only one parent.
Think of it like a family tree where each person has only one parent. The child inherits traits from the parent, and can also have unique traits of their own. It's simple, clear, and easy to understand.
Single inheritance is the most common type of inheritance because it's straightforward and avoids many of the complexities that can arise with multiple inheritance.
💡 Key concept: Single inheritance means one child class inherits from one parent class. It's the simplest and most common form of inheritance.
Creating Parent and Child Classes
The Syntax of Single Inheritance
Creating a single inheritance relationship is straightforward. You define a parent class, then define a child class that inherits from it by putting the parent class name in parentheses.
# The syntax of single inheritance
# Parent class
class Animal:
"""A class representing an animal"""
def __init__(self, name, species):
self.name = name
self.species = species
def eat(self):
return f"{self.name} is eating"
def sleep(self):
return f"{self.name} is sleeping"
def get_info(self):
return f"{self.name} is a {self.species}"
# Child class inheriting from Animal
class Dog(Animal):
"""A class representing a dog"""
def __init__(self, name, breed):
# Call the parent class's constructor
super().__init__(name, "Dog")
self.breed = breed
def bark(self):
return f"{self.name} says Woof!"
def fetch(self):
return f"{self.name} is fetching the ball"
# Let's see it in action
print("Creating a Dog object...")
my_dog = Dog("Buddy", "Golden Retriever")
# Using methods inherited from Animal
print(my_dog.eat()) # Buddy is eating
print(my_dog.sleep()) # Buddy is sleeping
print(my_dog.get_info()) # Buddy is a Dog
# Using methods defined in Dog
print(my_dog.bark()) # Buddy says Woof!
print(my_dog.fetch()) # Buddy is fetching the ball
# Checking the inheritance relationship
print(f"Is my_dog an Animal? {isinstance(my_dog, Animal)}") # True
print(f"Is Dog a subclass of Animal? {issubclass(Dog, Animal)}") # True
Single inheritance key points:
- Syntax — class Child(Parent):
- Parent class — the class being inherited from
- Child class — the class that inherits
- One parent only — child has exactly one parent
- Inherits everything — child gets all parent methods and attributes
- Can add new methods — child can extend functionality
Quick Check: How many parents does a class in single inheritance have? (Answer: Exactly one)
What Does a Child Class Inherit?
Understanding What Gets Passed Down
A child class inherits all the methods and attributes of its parent class. This includes instance methods, class methods, static methods, and class variables. It's a powerful way to reuse code.
# What does a child class inherit?
class Vehicle:
"""A parent class with various members"""
# Class variable
vehicle_count = 0
def __init__(self, brand, model):
# Instance attributes
self.brand = brand
self.model = model
Vehicle.vehicle_count += 1
# Instance method
def start(self):
return f"{self.brand} {self.model} is starting"
def stop(self):
return f"{self.brand} {self.model} is stopping"
# Class method
@classmethod
def get_count(cls):
return f"Total vehicles: {cls.vehicle_count}"
# Static method
@staticmethod
def honk():
return "Honk Honk!"
class Car(Vehicle):
"""A child class inheriting from Vehicle"""
def __init__(self, brand, model, doors=4):
super().__init__(brand, model)
self.doors = doors
def open_trunk(self):
return f"Opening the trunk of {self.brand} {self.model}"
# Let's see what Car inherits
car = Car("Toyota", "Camry", 4)
# Inherits instance attributes
print(car.brand) # Toyota
print(car.model) # Camry
# Inherits instance methods
print(car.start()) # Toyota Camry is starting
print(car.stop()) # Toyota Camry is stopping
# Inherits class variables
print(Vehicle.vehicle_count) # 1
# Inherits class methods
print(Car.get_count()) # Total vehicles: 1
# Inherits static methods
print(Car.honk()) # Honk Honk!
# Has its own methods
print(car.open_trunk()) # Opening the trunk of Toyota Camry
# Checking what Car inherited
print("\nCar class members:")
for item in dir(Car):
if not item.startswith('_'):
print(f" {item}")
# brand (inherited from __init__), model, doors, start, stop, get_count, honk, open_trunk
What a child class inherits:
- Instance methods — all methods defined in the parent
- Class methods — methods decorated with @classmethod
- Static methods — methods decorated with @staticmethod
- Class variables — variables defined at the class level
- Instance attributes — through calling super().__init__()
- Does not inherit — private attributes (those starting with __)
Quick Check: Does a child class inherit private attributes from the parent? (Answer: No, private attributes are not inherited)
Method Overriding in Single Inheritance
Customizing Inherited Methods
Method overriding allows a child class to provide its own implementation of a method that's already defined in the parent class. This is one of the most powerful features of inheritance.
# Method overriding in single inheritance
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
return "Some generic animal sound"
def move(self):
return f"{self.name} is moving"
def eat(self):
return f"{self.name} is eating"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
# Override the make_sound method
def make_sound(self):
return f"{self.name} says Woof!"
# Override the move method
def move(self):
return f"{self.name} is running"
# Add a new method
def fetch(self):
return f"{self.name} is fetching the ball"
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name)
self.color = color
# Override the make_sound method
def make_sound(self):
return f"{self.name} says Meow!"
# Override the move method
def move(self):
return f"{self.name} is walking silently"
def purr(self):
return f"{self.name} is purring"
class Bird(Animal):
def __init__(self, name, species):
super().__init__(name)
self.species = species
# Override the make_sound method
def make_sound(self):
return f"{self.name} chirps"
# Override the move method
def move(self):
return f"{self.name} is flying"
def lay_eggs(self):
return f"{self.name} laid an egg"
# Creating objects
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")
bird = Bird("Tweety", "Canary")
# Each class has its own behavior for the same method
print(dog.make_sound()) # Buddy says Woof!
print(cat.make_sound()) # Whiskers says Meow!
print(bird.make_sound()) # Tweety chirps
# Each class moves differently
print(dog.move()) # Buddy is running
print(cat.move()) # Whiskers is walking silently
print(bird.move()) # Tweety is flying
# Each class has its own unique methods
print(dog.fetch()) # Buddy is fetching the ball
print(cat.purr()) # Whiskers is purring
print(bird.lay_eggs()) # Tweety laid an egg
Method overriding key points:
- Same name — the method name must match the parent
- Different implementation — child provides its own version
- Customization — each child can have different behavior
- Parent method still exists — can be called with super()
- Polymorphism — the same method name works differently for different classes
Quick Check: What is method overriding? (Answer: When a child class provides its own implementation of a parent method)
Using super() in Single Inheritance
Calling Parent Methods
The super() function is essential in inheritance. It lets you call methods from the parent class, which is especially useful when you want to extend rather than completely replace the parent's behavior.
# Using super() in single inheritance
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
print(f"Person __init__ called for {name}")
def introduce(self):
return f"Hi, I'm {self.name}, {self.age} years old"
def work(self):
return f"{self.name} is working"
class Student(Person):
def __init__(self, name, age, student_id, major):
# Calling parent __init__ with super()
super().__init__(name, age)
self.student_id = student_id
self.major = major
print(f"Student __init__ called for {name}")
# Overriding introduce, but using parent's version
def introduce(self):
parent_intro = super().introduce()
return f"{parent_intro} and I'm a {self.major} student"
# Overriding work, but using parent's version
def work(self):
parent_work = super().work()
return f"{parent_work} and studying {self.major}"
def study(self):
return f"{self.name} is studying {self.major}"
class GraduateStudent(Student):
def __init__(self, name, age, student_id, major, thesis_topic):
# Calling parent __init__ with super()
super().__init__(name, age, student_id, major)
self.thesis_topic = thesis_topic
print(f"GraduateStudent __init__ called for {name}")
# Overriding introduce, using both parent versions
def introduce(self):
# Start with the parent's version
parent_intro = super().introduce()
return f"{parent_intro} and I'm writing a thesis on {self.thesis_topic}"
def research(self):
return f"{self.name} is researching {self.thesis_topic}"
# Creating objects
print("Creating a Student...")
student = Student("Alice", 20, "S001", "Computer Science")
print(student.introduce()) # Hi, I'm Alice, 20 years old and I'm a Computer Science student
print(student.work()) # Alice is working and studying Computer Science
print("\nCreating a GraduateStudent...")
grad = GraduateStudent("Bob", 25, "G001", "Physics", "Quantum Computing")
print(grad.introduce()) # Hi, I'm Bob, 25 years old and I'm a Physics student and I'm writing a thesis on Quantum Computing
print(grad.research()) # Bob is researching Quantum Computing
# The chain of constructor calls
# Person.__init__ → Student.__init__ → GraduateStudent.__init__
super() key points:
- Calls parent methods — allows you to use parent class functionality
- Used in __init__ — to initialize parent attributes
- Used in overriding — to extend parent methods
- Chain of calls — in multilevel inheritance, super() follows the chain
- Clean and maintainable — changes to parent automatically propagate
Quick Check: What does super() do in single inheritance? (Answer: It calls methods from the parent class)
Constructors in Inheritance
Handling Initialization in Parent and Child
When you have inheritance, you need to think about how objects are initialized. The parent class's __init__ method should be called from the child class to ensure that all attributes are properly set up.
# Constructors in inheritance
class Product:
"""A class representing a product"""
def __init__(self, product_id, name, price):
self.product_id = product_id
self.name = name
self.price = price
self.in_stock = True
print(f"Product __init__: {name} created")
def get_info(self):
return f"{self.name} (ID: {self.product_id}) - ${self.price}"
class ElectronicProduct(Product):
"""A class representing an electronic product"""
def __init__(self, product_id, name, price, brand, warranty_months):
# Call parent __init__
super().__init__(product_id, name, price)
self.brand = brand
self.warranty_months = warranty_months
print(f"ElectronicProduct __init__: {name} created")
def get_info(self):
parent_info = super().get_info()
return f"{parent_info} - Brand: {self.brand}, Warranty: {self.warranty_months} months"
class Smartphone(ElectronicProduct):
"""A class representing a smartphone"""
def __init__(self, product_id, name, price, brand, warranty_months, screen_size):
# Call parent __init__
super().__init__(product_id, name, price, brand, warranty_months)
self.screen_size = screen_size
self.os_version = "Latest"
print(f"Smartphone __init__: {name} created")
def get_info(self):
parent_info = super().get_info()
return f"{parent_info} - Screen: {self.screen_size} inches, OS: {self.os_version}"
def update_os(self, version):
self.os_version = version
return f"{self.name} updated to {version}"
# Creating objects
print("Creating a Product...")
product = Product("P001", "Book", 19.99)
print(product.get_info())
print("\nCreating an ElectronicProduct...")
laptop = ElectronicProduct("E001", "Laptop", 999.99, "Dell", 24)
print(laptop.get_info())
print("\nCreating a Smartphone...")
phone = Smartphone("S001", "Phone", 599.99, "Apple", 12, 6.1)
print(phone.get_info())
# The chain of constructor calls
# Product.__init__ → ElectronicProduct.__init__ → Smartphone.__init__
Constructors key points:
- Call parent __init__ — always call super().__init__() in child classes
- Chain of initialization — parent constructor runs first, then child
- Add child-specific attributes — after calling super()
- Don't forget — if you don't call super().__init__(), parent attributes won't be set
- Default values — child can add default values for new attributes
Quick Check: Why should you call super().__init__() in a child class? (Answer: To initialize the parent class's attributes)
Real-World Examples
Seeing Single Inheritance in Action
# Real-world example: A Library Management System
class LibraryItem:
"""Base class for all library items"""
def __init__(self, item_id, title, location):
self.item_id = item_id
self.title = title
self.location = location
self.is_borrowed = False
def borrow(self):
if not self.is_borrowed:
self.is_borrowed = True
return f"{self.title} has been borrowed"
return f"{self.title} is already borrowed"
def return_item(self):
if self.is_borrowed:
self.is_borrowed = False
return f"{self.title} has been returned"
return f"{self.title} was not borrowed"
def get_info(self):
status = "Borrowed" if self.is_borrowed else "Available"
return f"{self.title} (ID: {self.item_id}) - {status}"
class Book(LibraryItem):
"""A class representing a book"""
def __init__(self, item_id, title, location, author, pages):
super().__init__(item_id, title, location)
self.author = author
self.pages = pages
self.is_reference = False
def get_info(self):
parent_info = super().get_info()
return f"{parent_info} - by {self.author}, {self.pages} pages"
class DVD(LibraryItem):
"""A class representing a DVD"""
def __init__(self, item_id, title, location, director, duration):
super().__init__(item_id, title, location)
self.director = director
self.duration = duration
self.genre = "General"
def get_info(self):
parent_info = super().get_info()
return f"{parent_info} - Directed by {self.director}, {self.duration} min"
class Magazine(LibraryItem):
"""A class representing a magazine"""
def __init__(self, item_id, title, location, issue_number):
super().__init__(item_id, title, location)
self.issue_number = issue_number
def get_info(self):
parent_info = super().get_info()
return f"{parent_info} - Issue #{self.issue_number}"
# Using the library system
book = Book("B001", "Python Programming", "Shelf A", "John Smith", 350)
dvd = DVD("D001", "The Matrix", "Shelf B", "Wachowski", 136)
magazine = Magazine("M001", "Tech Today", "Shelf C", "42")
print("=== Library Items ===")
print(book.get_info()) # Python Programming (ID: B001) - Available - by John Smith, 350 pages
print(dvd.get_info()) # The Matrix (ID: D001) - Available - Directed by Wachowski, 136 min
print(magazine.get_info()) # Tech Today (ID: M001) - Available - Issue #42
print("\n=== Borrowing ===")
print(book.borrow()) # Python Programming has been borrowed
print(book.borrow()) # Python Programming is already borrowed
print("\n=== After Borrowing ===")
print(book.get_info()) # Python Programming (ID: B001) - Borrowed - by John Smith, 350 pages
print(book.return_item()) # Python Programming has been returned
print(book.get_info()) # Python Programming (ID: B001) - Available - by John Smith, 350 pages
Real-world example key points:
- Base class — LibraryItem provides common functionality
- Child classes — Book, DVD, Magazine add specific details
- Method overriding — each child customizes get_info()
- Code reuse — borrow() and return_item() are shared
- Extension — each child adds its own attributes
Quick Check: What does the LibraryItem base class provide? (Answer: Common functionality like borrowing and returning)
Best Practices for Single Inheritance
Using Single Inheritance Effectively
# Best practices for single inheritance
# 1. Use inheritance for "is-a" relationships
# Good: Dog is an Animal
class Animal: pass
class Dog(Animal): pass
# Bad: Car is an Engine (should be composition)
# class Car(Engine): pass
# 2. Keep the hierarchy shallow
# Good: 2-3 levels
class Vehicle: pass
class Car(Vehicle): pass
class SportsCar(Car): pass
# Bad: Very deep hierarchy
# class A: pass
# class B(A): pass
# class C(B): pass
# class D(C): pass
# class E(D): pass
# 3. Always call super().__init__()
class Parent:
def __init__(self, name):
self.name = name
class Child(Parent):
def __init__(self, name, age):
super().__init__(name) # Must call parent __init__
self.age = age
# Bad: Forgetting to call parent __init__
# class BadChild(Parent):
# def __init__(self, name, age):
# self.age = age
# # Name is not initialized!
# 4. Use method overriding to specialize behavior
class Shape:
def area(self):
return 0
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Override to provide specific behavior
return 3.14159 * self.radius ** 2
# 5. Use super() to extend parent methods
class Logger:
def log(self, message):
print(f"LOG: {message}")
class TimestampLogger(Logger):
def log(self, message):
import datetime
timestamp = datetime.datetime.now()
super().log(f"[{timestamp}] {message}") # Extend parent
# 6. Document the inheritance relationship
class Employee:
"""
Base class for all employees.
Subclasses:
Manager: Manages teams
Developer: Writes code
Designer: Creates designs
"""
pass
# 7. Keep classes focused on one responsibility
class DataProcessor:
"""Only processes data"""
pass
class DataValidator(DataProcessor):
"""Validates data before processing"""
pass
Best practices summary:
- Is-a relationships — use inheritance for "is-a" relationships
- Keep hierarchies shallow — avoid deep inheritance chains
- Call super().__init__() — always initialize parent attributes
- Override methods — to provide specialized behavior
- Use super() — to extend parent methods
- Document relationships — explain inheritance hierarchies
Quick Check: What should you always call in a child class's __init__? (Answer: super().__init__())
Try It Yourself
Experiment with single inheritance in the editor below.
SINGLE INHERITANCE PRACTICE
========================================
1. BASIC INHERITANCE
Buddy says Woof!
2. USING SUPER()
Vehicle __init__: Toyota
Car __init__: Camry
Brand: Toyota, Model: Camry
3. METHOD OVERRIDING
Rectangle area: 15
Single inheritance practice complete!
You've Got It!
You now understand single inheritance in Python. You know how to create parent and child classes, override methods, use super(), and work with constructors.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is the difference between single and multiple inheritance?
Can a child class have multiple parents in single inheritance?
What happens if I don't call super().__init__()?
super().__init__() in the child class's __init__ method, the parent class's attributes won't be initialized. This can lead to errors when you try to use parent attributes. It's always a good practice to call super().__init__().
What's a common interview question about single inheritance?
Can a child class override a parent method?
Is single inheritance the most common type?
Where to Go From Here
Now that you understand single inheritance, check out these related topics:
Multiple Inheritance
Learn about inheritance from multiple parents.
Learn More →Multilevel Inheritance
Learn about chains of inheritance.
Learn More →Method Overriding
Learn more about overriding methods in subclasses.
Learn More →