6.1 Single Inheritance, Method Overriding, and Polymorphism
Key Takeaways
- Inheritance establishes an 'is-a' relationship where subclasses inherit accessible attributes and methods from superclasses to enable hierarchical code reuse and specialization.
- Method overriding occurs when a subclass defines a method with the identical identifier as a superclass method, replacing or extending parent behavior during dynamic runtime dispatch.
- The zero-argument super() built-in in Python 3 proxies method calls to the parent class, ensuring proper superclass attribute initialization via super().__init__(...).
- The isinstance() function tests instance membership across inheritance trees (supporting class tuples), while issubclass() tests class-level derivation (where every class is a subclass of itself and object).
- Polymorphism in Python relies on duck typing ('if it walks and quacks like a duck, it is a duck'), enabling uniform operational interfaces without nominal type restrictions.
Single Inheritance, Method Overriding, and Polymorphism
Object-Oriented Programming (OOP) in Python is built upon the foundational pillars of encapsulation, abstraction, inheritance, and polymorphism. While basic OOP focuses on modeling entities using classes and instances, advanced OOP leverages inheritance hierarchies to establish relationships between concepts, reduce code duplication, and construct extensible software architectures.
Understanding how Python handles class derivation, method resolution, superclass delegation via super(), runtime type introspection, and dynamic polymorphism is essential for mastering Python and excelling on the OpenEDG PCAP-31-03 certification exam.
1. Concepts and Terminology of Inheritance
Inheritance is a mechanism where a new class (known as a subclass, derived class, or child class) is created based on an existing class (known as a superclass, base class, or parent class).
Inheritance models an "is-a" relationship. For example:
- A
Caris-aVehicle. - A
Manageris-aEmployee. - A
SecureSocketis-aSocket.
Syntax for Class Derivation
In Python, inheritance is declared by placing the name of the superclass in parentheses immediately following the subclass name in the class definition header:
class Superclass:
"""Parent base class."""
pass
class Subclass(Superclass):
"""Child derived class inheriting from Superclass."""
pass
Python's Universal Root: The object Class
In Python 3, all classes implicitly inherit from the built-in object base class if no explicit superclass is specified. The following two declarations are semantically identical:
# Implicit inheritance from object
class Appliance:
pass
# Explicit inheritance from object (redundant in Python 3)
class Appliance(object):
pass
The object class supplies standard dunder methods (such as __str__(), __repr__(), __eq__(), __hash__(), and __init__()) to every Python object.
Inheriting Attributes and Methods
When a subclass is instantiated, it automatically inherits all class attributes, instance methods, and behaviors defined on its superclass hierarchy without requiring manual redefinition:
class Vehicle:
category = "Land Transportation"
def __init__(self, brand, model):
self.brand = brand
self.model = model
def describe(self):
return f"{self.brand} {self.model} ({self.category})"
class Car(Vehicle):
# Car inherits category, __init__, and describe() automatically
pass
sedan = Car("Toyota", "Camry")
print(sedan.describe()) # Output: Toyota Camry (Land Transportation)
print(sedan.category) # Output: Land Transportation
2. Method Overriding
Method overriding occurs when a subclass defines a method with the exact same identifier (name) and parameter signature as a method present in its superclass. Overriding allows a subclass to provide a specialized implementation tailored to its specific requirements while preserving a uniform interface.
Dynamic Dispatch Mechanics
When a method is called on an instance (e.g., instance.method()), Python's attribute lookup inspects the instance's class first. If the method is defined directly in that class, Python executes it immediately, effectively shadowing (hiding) any identically named method in the parent class:
class Employee:
def __init__(self, name, base_salary):
self.name = name
self.base_salary = base_salary
def calculate_bonus(self):
# Standard employee bonus: 5% of base salary
return self.base_salary * 0.05
class Salesperson(Employee):
def __init__(self, name, base_salary, commission):
# We will explore super().__init__ shortly
self.name = name
self.base_salary = base_salary
self.commission = commission
def calculate_bonus(self):
# Method Overridden: Sales bonus includes 10% base + direct commission
return (self.base_salary * 0.10) + self.commission
emp = Employee("Alice", 60000)
seller = Salesperson("Bob", 60000, 5000)
print(emp.calculate_bonus()) # Output: 3000.0 (Standard 5% bonus)
print(seller.calculate_bonus()) # Output: 11000.0 (Overridden bonus calculation)
3. Superclass Delegation with super()
Often, an overriding subclass method should not completely replace superclass functionality, but rather extend it by executing parent logic and adding custom behavior. Hardcoding the parent class name (e.g., Employee.calculate_bonus(self)) introduces rigid coupling and breaks under complex inheritance hierarchies.
Python provides the built-in super() function as a dynamic proxy that delegates method calls to the appropriate superclass.
Zero-Argument super() in Python 3
In Python 3, super() is called inside method definitions with zero arguments. The compiler automatically supplies the current enclosing class and instance (self):
Chaining Constructors: super().__init__(...)
The most common application of super() is invoking the parent class's constructor inside the subclass __init__() method:
class Device:
def __init__(self, serial_number, power_rating):
self.serial_number = serial_number
self.power_rating = power_rating
self.is_powered = False
def power_on(self):
self.is_powered = True
class Laptop(Device):
def __init__(self, serial_number, power_rating, battery_capacity, os_name):
# Delegate common initialization to Device.__init__
super().__init__(serial_number, power_rating)
# Initialize subclass-specific attributes
self.battery_capacity = battery_capacity
self.os_name = os_name
def power_on(self):
# Extend Device.power_on with operating system boot message
super().power_on()
return f"Laptop {self.serial_number} booted into {self.os_name}."
macbook = Laptop("MBP-2026-X", 67, 70, "macOS")
print(macbook.is_powered) # Output: False (Initialized by Device.__init__)
print(macbook.power_on()) # Output: Laptop MBP-2026-X booted into macOS.
print(macbook.is_powered) # Output: True (Updated by Device.power_on via super())
[!WARNING] The Missing
super().__init__()Bug: If a subclass defines its own__init__()method and fails to callsuper().__init__(), the superclass's__init__()method is never executed. Consequently, instance attributes expected from the parent class will not be initialized, causing runtimeAttributeErrorexceptions when accessed.
class Parent:
def __init__(self):
self.parent_attr = "initialized"
class Child(Parent):
def __init__(self):
# Omitting super().__init__()
self.child_attr = "ready"
c = Child()
print(c.child_attr) # Output: ready
try:
print(c.parent_attr)
except AttributeError as err:
print("Caught error:", err) # Caught error: 'Child' object has no attribute 'parent_attr'
4. Runtime Type Inspection: isinstance() and issubclass()
Python provides two primary introspection built-in functions to verify class relationships and object types at runtime.
The isinstance(object, classinfo) Function
The isinstance() built-in checks whether an object instance is an instance of a given class or any subclass derived from it:
class Animal: pass
class Dog(Animal): pass
class Labrador(Dog): pass
my_pet = Labrador()
print(isinstance(my_pet, Labrador)) # True (Direct class instance)
print(isinstance(my_pet, Dog)) # True (Inherited from Dog)
print(isinstance(my_pet, Animal)) # True (Inherited from Animal)
print(isinstance(my_pet, object)) # True (All objects subclass object)
print(isinstance(my_pet, str)) # False (Unrelated type)
Tuple of Classes in isinstance()
classinfo can be passed as a tuple of types. isinstance() returns True if the object is an instance of any type in the tuple (logical OR):
val = 42
print(isinstance(val, (str, list, tuple))) # False
print(isinstance(val, (str, int, float))) # True (Matches int)
The issubclass(class, classinfo) Function
The issubclass() built-in inspects relationships between class objects (types), rather than instances:
print(issubclass(Labrador, Dog)) # True (Labrador derives from Dog)
print(issubclass(Labrador, Animal)) # True (Labrador derives indirectly from Animal)
print(issubclass(Dog, Labrador)) # False (Superclass is not a subclass of Child)
Fundamental Rules for issubclass() on the PCAP Exam
- Reflexivity: A class is always considered a subclass of itself:
- Universal Derivation: Every class in Python 3 is a subclass of
object: - Type Constraint: Both arguments must be class types. Passing an instance as the first argument raises a
TypeError:pet = Dog() try: issubclass(pet, Animal) # 'pet' is an instance, NOT a class! except TypeError as err: print("TypeError:", err) # issubclass() arg 1 must be a class - Tuple Support: Like
isinstance(),issubclass()accepts a tuple of target candidate classes.
Comparison Table: Type Introspection Mechanisms
| Mechanism | First Argument | Considers Subclasses? | Best Use Case |
|---|---|---|---|
type(obj) is Cls | Instance | No (Strict exact identity) | Exact type pinning without inheritance |
isinstance(obj, Cls) | Instance | Yes (Polymorphic membership) | Idiomatic runtime type checking |
issubclass(Sub, Cls) | Class Object | Yes (Class tree hierarchy) | Verifying API contract / class hierarchies |
5. Polymorphism and Duck Typing
Polymorphism (from Greek: "having multiple forms") refers to the programming capability where different classes can define methods sharing the identical interface (method name and parameter signature), allowing client code to treat instances of different classes uniformly without knowing their specific underlying types.
Nominal Polymorphism vs. Duck Typing
In statically typed languages (such as C++ or Java), polymorphism requires explicit nominal inheritance: all participating classes must inherit from a common abstract base class or implement a shared interface.
In Python, polymorphism is governed by Duck Typing:
"If it walks like a duck and quacks like a duck, it's a duck."
Duck typing prioritizes what an object can do (its methods and attributes) rather than what it is (its nominal class inheritance):
class PDFExporter:
def export(self, payload):
return f"Rendering PDF document with payload: {payload}"
class CSVExporter:
def export(self, payload):
return f"Writing CSV file with payload: {payload}"
class CloudSyncExporter:
def export(self, payload):
return f"Pushing JSON payload to cloud endpoint: {payload}"
# Polymorphic consumer function
def publish_report(exporter, data):
# The function only cares that 'exporter' has a callable 'export' method
result = exporter.export(data)
print("Status:", result)
# All three distinct types operate seamlessly without a shared base class
publish_report(PDFExporter(), {"total": 500})
publish_report(CSVExporter(), {"total": 500})
publish_report(CloudSyncExporter(), {"total": 500})
Dynamic Method Resolution at Runtime
Because Python resolves attribute and method lookups dynamically at the exact moment of execution (runtime dispatch), new classes can be introduced to an existing system at any time without modifying downstream consumers, providing immense architectural flexibility.
A developer executes the following code snippet:
What is the outcome of running this script?class Base:
def __init__(self):
self.value = 100
class Derived(Base):
def __init__(self):
self.multiplier = 2
obj = Derived()
print(obj.value * obj.multiplier)
Consider the following class definitions and expressions:
Which of the following expressions evaluates to False or raises an exception?class Device: pass
class Sensor(Device): pass
s = Sensor()
Given the following inheritance hierarchy:
Which statement accurately describes the type relationships for class Component: pass
class Button(Component): pass
class RadioButton(Button): pass
btn = Button()
btn?
Consider the following implementation of polymorphic document processors:
What enables class HTMLDoc:
def render(self):
return "<html/>"
class JSONDoc:
def render(self):
return "{}"
def display(doc):
return doc.render()
print(display(HTMLDoc()) + " " + display(JSONDoc()))
display() to successfully process both HTMLDoc and JSONDoc instances without an explicit shared base class?