Programming with Python | Chapter 13: OOP – Inheritance
Chapter Objectives
- Understand the concept of inheritance in OOP for Python.
- Differentiate between a base class (superclass/parent) and a derived class (subclass/child).
- Learn the syntax for creating a derived class that inherits from a base class.
- Understand how derived classes inherit attributes and methods from their base class.
- Learn how to override methods from the base class in the derived class.
- Use the
super()
function to call methods from the base class within the derived class, especially__init__
. - Recognize the “is-a” relationship represented by inheritance.
- Appreciate the benefits of inheritance for code reuse and creating hierarchies.
Introduction
Chapter 12 introduced classes and objects, allowing us to model entities with attributes and methods. Inheritance is a fundamental OOP principle that allows us to create a new class (the derived class or subclass) that inherits properties (attributes and methods) from an existing class (the base class or superclass). This promotes code reuse, as common functionality can be defined in the base class and automatically acquired by derived classes. Derived classes can then add their own unique attributes and methods or modify (override) the inherited behavior to specialize it. Inheritance models an “is-a” relationship (e.g., a Dog
is an Animal
, a Car
is a Vehicle
).
Theory & Explanation
What is Inheritance?
Inheritance is a mechanism where one class (the child/subclass/derived class) acquires the properties (attributes and methods) of another class (the parent/superclass/base class). The subclass can then:
- Use the inherited attributes and methods directly.
- Add new attributes and methods specific to itself.
- Override inherited methods to provide specialized behavior.
Analogy: Think of biological inheritance. You inherit traits (like eye color) from your parents, but you also have your own unique characteristics. Similarly, a subclass inherits features from its superclass but can also add its own or modify the inherited ones.
Base Class (Superclass) and Derived Class (Subclass)
- Base Class (Superclass/Parent): The class whose properties are inherited. It represents a more general concept.
- Derived Class (Subclass/Child): The class that inherits properties from the base class. It represents a more specialized concept.
classDiagram direction LR class Animal { +string name +string species +__init__(name, species) +speak() +move() } class Dog { +string breed +__init__(name, breed) // Overrides Animal.__init__ +speak() // Overrides Animal.speak +move() // Overrides Animal.move } class Cat { +__init__(name, species) // Inherits or overrides +speak() // Overrides Animal.speak } Animal <|-- Dog : Inherits Animal <|-- Cat : Inherits note for Animal "Base Class<br/>(Superclass)" note for Dog "Derived Class<br/>(Subclass)<br/>Uses super()" note for Cat "Derived Class<br/>(Subclass)"
Syntax for Inheritance
To create a derived class that inherits from a base class, you include the base class name in parentheses after the derived class name in the class definition.
Syntax:
class BaseClassName:
# Base class definition
pass # Placeholder for actual code
class DerivedClassName(BaseClassName): # Inherits from BaseClassName
# Derived class definition
# Can add new attributes/methods or override existing ones
pass
Inheriting Attributes and Methods
By default, a derived class inherits all the attributes and methods of its base class. Objects of the derived class can access these inherited members as if they were defined in the derived class itself (unless they are overridden).
class Animal:
"""Represents a general animal."""
def __init__(self, name, species):
self.name = name
self.species = species
print(f"Animal initialized: {self.name}")
def speak(self):
print("Some generic animal sound")
def move(self):
print("Moving...")
# Dog inherits from Animal
class Dog(Animal):
"""Represents a dog, inheriting from Animal."""
# Dog doesn't define its own __init__ or move yet,
# so it inherits them from Animal.
pass # For now, just inherit everything
# Create instances
generic_animal = Animal("Creature", "Unknown")
my_dog = Dog("Buddy", "Canis familiaris") # Calls Animal's __init__
# Access inherited attributes
print(f"Dog's name: {my_dog.name}") # Inherited from Animal
print(f"Dog's species: {my_dog.species}") # Inherited from Animal
# Call inherited methods
my_dog.move() # Output: Moving... (Inherited from Animal)
my_dog.speak() # Output: Some generic animal sound (Inherited from Animal)
Overriding Methods
A derived class can provide its own specific implementation of a method that it inherited from its base class. This is called method overriding. To override a method, simply define a method in the derived class with the same name and signature (parameters) as the one in the base class.
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def speak(self): # Base class method
print("Some generic animal sound")
# Dog inherits from Animal
class Dog(Animal):
def __init__(self, name, breed): # Dog's own __init__
# We need to initialize the Animal part too! (See super() next)
self.name = name # Temporarily set name directly
self.breed = breed
print(f"Dog initialized: {self.name}")
def speak(self): # Overrides Animal's speak method
print("Woof! Woof!")
# Cat inherits from Animal
class Cat(Animal):
def speak(self): # Overrides Animal's speak method
print("Meow!")
# Create instances
d = Dog("Rex", "German Shepherd")
c = Cat("Whiskers", "Felis catus")
d.speak() # Output: Woof! Woof! (Calls Dog's overridden version)
c.speak() # Output: Meow! (Calls Cat's overridden version)
When speak()
is called on a Dog
object, Python executes the speak
method defined in the Dog
class. If Dog
hadn’t defined speak
, Python would have looked up the inheritance chain and executed Animal
‘s speak
method.
Calling Base Class Methods: super()
Often, when overriding a method (especially __init__
), you still want to execute the original implementation from the base class before or after adding the subclass’s specific logic. The super()
function provides a way to access methods of the base class (superclass).
super().__init__(...)
: Commonly used within the subclass’s__init__
to call the base class’s__init__
, ensuring that the base class attributes are properly initialized.super().method_name(...)
: Used to call any other method from the base class.
class Animal:
def __init__(self, name, species):
print(f"Animal __init__ called for {name}")
self.name = name
self.species = species
def move(self):
print(f"{self.name} is moving.")
class Dog(Animal):
def __init__(self, name, breed):
print(f"Dog __init__ called for {name}")
# Call the __init__ method of the base class (Animal)
super().__init__(name, species="Canis familiaris")
# Now add Dog-specific attributes
self.breed = breed
def move(self): # Override move
# Call the base class move method first
super().move()
# Add Dog-specific moving behavior
print(f"{self.name} is running excitedly!")
# Create instance
buddy = Dog("Buddy", "Labrador")
# Output:
# Dog __init__ called for Buddy
# Animal __init__ called for Buddy
print(f"Buddy's species: {buddy.species}") # Initialized by Animal's __init__ via super()
print(f"Buddy's breed: {buddy.breed}") # Initialized by Dog's __init__
buddy.move()
# Output:
# Buddy is moving. (from super().move())
# Buddy is running excitedly! (from Dog's move())
Using super()
is crucial for proper initialization in subclasses and for extending, rather than completely replacing, inherited behavior.
The “is-a” Relationship
Inheritance models an “is-a” relationship. A Dog
is an Animal
. A Car
is a Vehicle
. This means an object of the derived class can be treated as an object of the base class in many contexts (this relates to polymorphism, covered later). If the relationship isn’t truly “is-a”, inheritance might not be the best approach (composition might be better – having an object contain another object).
Code Examples
Example 1: Vehicle Hierarchy
# vehicle_hierarchy.py
class Vehicle:
"""Represents a general vehicle."""
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.is_started = False
print(f"Vehicle created: {self.year} {self.make} {self.model}")
def start_engine(self):
if not self.is_started:
self.is_started = True
print(f"{self.make} {self.model}'s engine started.")
else:
print("Engine is already running.")
def stop_engine(self):
if self.is_started:
self.is_started = False
print(f"{self.make} {self.model}'s engine stopped.")
else:
print("Engine is already stopped.")
def get_info(self):
"""Returns basic vehicle information."""
return f"{self.year} {self.make} {self.model}"
# Car inherits from Vehicle
class Car(Vehicle):
"""Represents a car, inheriting from Vehicle."""
def __init__(self, make, model, year, num_doors):
# Call parent __init__
super().__init__(make, model, year)
# Add car-specific attribute
self.num_doors = num_doors
print(f"-> It's a car with {self.num_doors} doors.")
# Override get_info to add door count
def get_info(self):
# Get base info using super()
base_info = super().get_info()
return f"{base_info}, Doors: {self.num_doors}"
# Motorcycle inherits from Vehicle
class Motorcycle(Vehicle):
"""Represents a motorcycle, inheriting from Vehicle."""
def __init__(self, make, model, year, type): # e.g., 'Cruiser', 'Sport'
super().__init__(make, model, year)
self.type = type
print(f"-> It's a {self.type} motorcycle.")
# Override start_engine for a different sound/message
def start_engine(self):
if not self.is_started:
self.is_started = True
print(f"{self.make} {self.model}'s engine roars to life!") # Different message
else:
print("Engine is already running.")
# --- Using the classes ---
my_car = Car("Toyota", "Corolla", 2021, 4)
my_bike = Motorcycle("Harley-Davidson", "Street Bob", 2023, "Cruiser")
print("\n--- Car Info ---")
print(my_car.get_info())
my_car.start_engine()
my_car.stop_engine()
print("\n--- Motorcycle Info ---")
print(my_bike.get_info())
my_bike.start_engine()
my_bike.start_engine() # Try starting again
classDiagram direction LR class Vehicle { +string make +string model +int year +bool is_started +__init__(make, model, year) +start_engine() +stop_engine() +get_info() string } class Car { +int num_doors +__init__(make, model, year, num_doors) +get_info() string // Overrides Vehicle.get_info } class Motorcycle { +string type +__init__(make, model, year, type) +start_engine() // Overrides Vehicle.start_engine } Vehicle <|-- Car : Inherits Vehicle <|-- Motorcycle : Inherits note for Vehicle "Base Class" note for Car "Derived Class (Subclass)" note for Motorcycle "Derived Class (Subclass)"
Explanation:
Vehicle
is the base class with common attributes (make
,model
,year
,is_started
) and methods (start_engine
,stop_engine
,get_info
).Car
inherits fromVehicle
. Its__init__
callssuper().__init__()
to initialize the common attributes and then addsnum_doors
. It overridesget_info()
to include the door count, callingsuper().get_info()
to get the base information.Motorcycle
also inherits fromVehicle
. Its__init__
calls the parent__init__
and adds atype
attribute. It overridesstart_engine()
to provide a different message.- Objects of
Car
andMotorcycle
can use the inheritedstop_engine
method directly but use their own specialized versions of__init__
,get_info
(forCar
), andstart_engine
.
Common Mistakes or Pitfalls
Forgetting super().__init__():
When a subclass defines its own__init__
, forgetting to callsuper().__init__()
means the base class’s initialization logic (setting up its attributes) is skipped, often leading toAttributeError
later when inherited methods try to access those missing attributes.Incorrect super() Arguments:
Passing the wrong number or type of arguments tosuper().__init__()
or othersuper().method()
calls. The arguments must match what the corresponding base class method expects.- Overriding vs. Overloading: Python doesn’t support traditional method overloading (multiple methods with the same name but different parameter types) in the same way as languages like Java or C++. Defining a method with the same name always overrides the base class version. Default arguments or variable arguments (
*args
,**kwargs
) are used instead of overloading. - Confusing Inheritance (“is-a”) with Composition (“has-a”): Using inheritance when the relationship isn’t truly “is-a”. For example, a
Car
has anEngine
, but anEngine
is not aCar
. In this case, theCar
class should contain anEngine
object as an attribute (composition) rather than inheriting fromEngine
.
Chapter Summary
Concept | Description | Syntax / Example |
---|---|---|
Inheritance | Mechanism where a new class (derived/subclass) acquires properties (attributes, methods) from an existing class (base/superclass). | Models an “is-a” relationship (e.g., a Dog is an Animal ). |
Base Class (Superclass) | The class whose properties are inherited. Represents a more general concept. | class Vehicle: ... |
Derived Class (Subclass) | The class that inherits properties. Represents a more specialized concept. Can add/override members. | class Car(Vehicle): ... |
Syntax | Specify the base class in parentheses after the derived class name. | class DerivedClass(BaseClass): |
Method Overriding | Providing a specific implementation in the subclass for a method already defined in the base class. The subclass version is used for subclass objects. | Defining speak() in Dog overrides speak() from Animal . |
super() |
Function to call methods from the base class within the subclass. Essential for initializing the base part (super().__init__(...) ) and extending, not replacing, functionality (super().method(...) ). |
super().__init__(make, model, year) base_info = super().get_info() |
“is-a” Relationship | Inheritance signifies that the derived class *is a* type of the base class. If this isn’t true, composition (“has-a”) might be better. | A Car is a Vehicle . A Manager is an Employee . |
Benefits | Code reuse (common logic in base class), extensibility (add specific features in subclass), maintainability (changes in base class affect subclasses), modeling hierarchical relationships. | Vehicle hierarchy (Vehicle -> Car, Motorcycle), Animal hierarchy (Animal -> Dog, Cat). |
- Inheritance allows a derived class (subclass) to inherit attributes and methods from a base class (superclass).
- Use the syntax
class DerivedClass(BaseClass):
to define inheritance. - Subclasses automatically gain access to superclass members.
- Subclasses can add new attributes and methods.
- Subclasses can override inherited methods by defining a method with the same name.
- Use
super()
to call methods from the base class within the subclass (e.g.,super().__init__(...)
,super().method(...)
). - Inheritance models an “is-a” relationship and promotes code reuse.
Exercises & Mini Projects
Exercises
Employee
andManager
:- Create a base class
Employee
with__init__(self, name, salary)
and a methoddisplay_employee(self)
that prints the name and salary. - Create a derived class
Manager
that inherits fromEmployee
. - In the
Manager
‘s__init__(self, name, salary, department)
, call theEmployee
‘s__init__
usingsuper()
and add aself.department
attribute. - Override the
display_employee(self)
method inManager
to also print the department (it can optionally call the parent’s method usingsuper()
first). - Create instances of both
Employee
andManager
and call theirdisplay_employee
methods.
- Create a base class
- Shapes:
- Create a base class
Shape
with an__init__(self, color)
and a methoddescribe(self)
that prints “This shape is [color]”. - Create a derived class
Rectangle
inheriting fromShape
. Its__init__
should takecolor
,width
, andheight
. Call the parent__init__
and store width/height. - Add a method
area(self)
toRectangle
that returns the area. - Override the
describe(self)
method inRectangle
to print “This rectangle is [color] with width [width] and height [height]”. - Create a
Rectangle
object, calldescribe()
, and print its area.
- Create a base class
- Calling Parent Method: Using the
Employee
/Manager
classes from Exercise 1, modify theManager
‘sdisplay_employee
method so it first calls theEmployee
‘sdisplay_employee
method usingsuper()
and then prints the department information on a new line.
Mini Project: Library Items Hierarchy
Goal: Extend the Book
class from Chapter 12’s mini-project using inheritance to represent different types of library items.
Steps:
Rename Book to LibraryItem (Base Class):
- Take the
Book
class definition from Chapter 12. - Rename the class to
LibraryItem
. - Keep the
__init__(self, title, identifier)
(changeisbn
to a more genericidentifier
). Keepis_checked_out
. - Keep the
display_info
,check_out
, andcheck_in
methods, adjusting them to useidentifier
if necessary. Makedisplay_info
print title and identifier.
- Take the
Create Book Subclass:
- Create a new class
Book
that inherits fromLibraryItem
. - Define its
__init__(self, title, identifier, author, num_pages)
. - Inside
Book
‘s__init__
, call theLibraryItem
‘s__init__
usingsuper()
to initializetitle
,identifier
, andis_checked_out
. - Add the new instance attributes
self.author
andself.num_pages
. - Override the
display_info(self)
method inBook
. It should first callsuper().display_info()
to print the title/identifier, and then print the author and number of pages on new lines.
- Create a new class
Create DVD Subclass:
- Create another class
DVD
that also inherits fromLibraryItem
. - Define its
__init__(self, title, identifier, director, duration_minutes)
. - Call the parent
__init__
usingsuper()
. - Add the new instance attributes
self.director
andself.duration_minutes
. - Override the
display_info(self)
method inDVD
. Callsuper().display_info()
first, then print the director and duration.
- Create another class
- Instantiate and Use:
- Create at least one
Book
object and oneDVD
object. - Call
display_info()
on both objects to see the specialized output. - Call
check_out()
andcheck_in()
on both objects (these methods are inherited directly fromLibraryItem
and should work without modification).
- Create at least one
Additional Sources: