Python Tutorials

Python Inheritance & Polymorphism : OOP super()

Imagine you are designing a video game with dozens of different enemies. Every enemy needs a health bar, a movement speed, and a method to take damage. If you code these exact same features from scratch for the Goblin, the Dragon, and the Skeleton, you will waste hundreds of hours duplicating code.

What if, instead, you created a single, generic Enemy blueprint containing all that shared logic, and then told the Goblin and Dragon to “inherit” those features?

This is the magic of Inheritance. It allows you to write your code once and reuse it across multiple objects. When you combine this with Polymorphism—the ability for the Dragon to “attack” by breathing fire while the Goblin “attacks” by swinging a club, even though they share the same base command—you unlock the true power and scalability of Object-Oriented Programming (OOP) in Python.

What is Inheritance & Polymorphism?

  • Inheritance: A mechanism where a new class (the Child or Subclass) derives properties and behaviors (attributes and methods) from an existing class (the Parent or Superclass). This establishes an “is-a” relationship (e.g., a Car is a Vehicle).
  • Method Overriding: A feature that allows a child class to provide a specific implementation of a method that is already provided by its parent class.
  • Polymorphism: Derived from Greek meaning “many forms.” In Python, it refers to the ability of different objects to respond to the exact same method call in their own unique ways.

Syntax & Basic Usage

To make a class inherit from another, you simply pass the Parent class as an argument into the parentheses of the Child class definition.

# 1. The Parent Class (Base Class)
class Animal:
    def __init__(self):
        self.is_alive = True

    def eat(self):
        print("This animal is eating food.")

# 2. The Child Class (Subclass) inheriting from Animal
class Dog(Animal):
    # The Dog gets its own unique method...
    def bark(self):
        print("Woof! Woof!")

# 3. Using the Child Class
my_dog = Dog()

# The Dog can use its own method
my_dog.bark()

# AND it can use the method inherited from the Animal parent!
my_dog.eat()
print(f"Is the dog alive? {my_dog.is_alive}")

# Expected Output:
# Woof! Woof!
# This animal is eating food.
# Is the dog alive? True

Inheritance, Polymorphism, and Passing Function Arguments

Inheriting basic methods is just the beginning. To build professional applications, you must understand how to modify parent behaviors, pass arguments up the inheritance chain, and handle multiple parent classes simultaneously.

1. Overriding Methods

When a child class inherits a method from a parent, it doesn’t have to keep the parent’s exact behavior. The child can override the method by redefining it with the exact same name. Python will always prioritize the child’s version of the method.

class GenericSmartPhone:
    def unlock_screen(self):
        print("Screen unlocked via PIN code.")

class AdvancedSmartPhone(GenericSmartPhone):
    # We OVERRIDE the parent method by redefining it
    def unlock_screen(self):
        print("Screen unlocked via Facial Recognition.")

basic_phone = GenericSmartPhone()
new_phone = AdvancedSmartPhone()

basic_phone.unlock_screen()
new_phone.unlock_screen()

# Expected Output:
# Screen unlocked via PIN code.
# Screen unlocked via Facial Recognition.

2. Passing Function Arguments to Parent Classes with super()

If a child class overrides the parent’s __init__ constructor to add new attributes, the parent’s constructor is completely blocked. This means the parent’s attributes will never be created!

To solve this, we use the super() function. super() acts as a direct telephone line to the parent class, allowing us to pass function arguments up the chain so the parent can initialize its own data.

class Employee:
    # The parent requires 'name' and 'salary' arguments
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
        print(f"Employee {self.name} created.")

class Manager(Employee):
    # The child requires 'name', 'salary', AND 'team_size'
    def __init__(self, name, salary, team_size):
        # 1. Pass the relevant arguments UP to the Parent's __init__ using super()
        super().__init__(name, salary)
        
        # 2. Initialize the Child's unique attribute
        self.team_size = team_size
        print(f"Manager {self.name} now manages a team of {self.team_size}.")

# Instantiate the child class
tech_manager = Manager("Sarah", 120000, 5)

print(f"Salary verified: ${tech_manager.salary}")

# Expected Output:
# Employee Sarah created.
# Manager Sarah now manages a team of 5.
# Salary verified: $120000

3. Polymorphism in Action

Polymorphism allows us to treat completely different objects as if they were the exact same type, as long as they share the same method names. This allows us to write highly dynamic loops that process thousands of different objects seamlessly.

class Circle:
    def calculate_area(self):
        return "Area = pi * radius^2"

class Square:
    def calculate_area(self):
        return "Area = side * side"

class Triangle:
    def calculate_area(self):
        return "Area = 0.5 * base * height"

# We have a list of completely different objects
shape_collection = [Circle(), Square(), Triangle()]

# Polymorphism allows us to call the EXACT SAME method on all of them
for shape in shape_collection:
    print(shape.calculate_area())

# Expected Output:
# Area = pi * radius^2
# Area = side * side
# Area = 0.5 * base * height

4. Multiple Inheritance

Unlike some programming languages (like Java), Python supports Multiple Inheritance. A single child class can inherit from two entirely different parent classes at the same time.

class Camera:
    def take_photo(self):
        print("Snapshot taken!")

class GPS:
    def get_location(self):
        print("Current location: 40.7128° N, 74.0060° W")

# The SmartWatch inherits from BOTH Camera and GPS
class SmartWatch(Camera, GPS):
    def display_time(self):
        print("The time is 12:45 PM.")

my_watch = SmartWatch()

# The child object has access to methods from all parents!
my_watch.display_time()
my_watch.take_photo()
my_watch.get_location()

# Expected Output:
# The time is 12:45 PM.
# Snapshot taken!
# Current location: 40.7128° N, 74.0060° W

Real-World Practical Examples

Scenario 1: Payment Processing System

In e-commerce, you want the checkout process to be unified, regardless of how the user pays. We use an abstract base class to enforce the structure, and polymorphism to handle the specific logic.

# The Parent Blueprint
class PaymentProcessor:
    def process_payment(self, amount):
        # We explicitly raise an error here to force children to override this method!
        raise NotImplementedError("Subclasses must implement this method.")

# Child 1
class CreditCardPayment(PaymentProcessor):
    def process_payment(self, amount):
        print(f"Connecting to Visa/Mastercard network... Processed ${amount}.")

# Child 2
class PayPalPayment(PaymentProcessor):
    def process_payment(self, amount):
        print(f"Routing to PayPal secure servers... Processed ${amount}.")

# Child 3
class CryptoPayment(PaymentProcessor):
    def process_payment(self, amount):
        print(f"Verifying blockchain transaction... Processed ${amount}.")

# The Checkout Function utilizes Polymorphism
def checkout(payment_method, cart_total):
    print("Initiating checkout...")
    # It doesn't matter WHAT the payment method is, as long as it has 'process_payment'
    payment_method.process_payment(cart_total)

# Simulating different users checking out
checkout(CreditCardPayment(), 150.00)
checkout(CryptoPayment(), 5000.00)

# Expected Output:
# Initiating checkout...
# Connecting to Visa/Mastercard network... Processed $150.0.
# Initiating checkout...
# Verifying blockchain transaction... Processed $5000.0.

Scenario 2: UI Component Library

When building user interfaces, every button, checkbox, and text field is a “UI Component”. They all share basic rendering logic, but have distinct behaviors.

class UIComponent:
    def __init__(self, x_position, y_position):
        self.x = x_position
        self.y = y_position

    def render(self):
        print(f"Drawing component at ({self.x}, {self.y}).")

class Button(UIComponent):
    def __init__(self, x_position, y_position, label_text):
        # Pass coordinates to parent
        super().__init__(x_position, y_position)
        self.label = label_text

    # Override render to add specific button graphics
    def render(self):
        super().render() # Call the parent's drawing logic first
        print(f"--> Adding button border and text: '{self.label}'")

class Checkbox(UIComponent):
    def __init__(self, x_position, y_position, is_checked=False):
        super().__init__(x_position, y_position)
        self.checked = is_checked

    def render(self):
        super().render()
        status = "[X]" if self.checked else "[ ]"
        print(f"--> Adding checkbox graphic: {status}")

# Render the whole screen
screen_elements = [
    Button(10, 20, "Submit"),
    Checkbox(10, 50, True),
    Button(100, 20, "Cancel")
]

for element in screen_elements:
    element.render()
    print("-" * 30)

# Expected Output:
# Drawing component at (10, 20).
# --> Adding button border and text: 'Submit'
# ------------------------------
# Drawing component at (10, 50).
# --> Adding checkbox graphic: [X]
# ------------------------------
# Drawing component at (100, 20).
# --> Adding button border and text: 'Cancel'
# ------------------------------

Best Practices & Common Pitfalls

  • Forgetting super().__init__(): The most common mistake beginners make is defining an __init__ method in a child class and forgetting to call super().__init__(). This entirely skips the parent’s setup process, causing AttributeErrors later when the child tries to access a variable the parent was supposed to create.
  • The “Is-A” vs “Has-A” Rule: Only use inheritance when a genuine “Is-A” relationship exists (e.g., A Dog is an Animal). Do not use inheritance to just steal code. If a Car needs an Engine, the Car should not inherit from Engine (a Car is not an Engine). Instead, the Car should contain an Engine instance as an attribute (this is called Composition, or a “Has-A” relationship).
  • Method Resolution Order (MRO) Complexity: If you use Multiple Inheritance (e.g., Class C inherits from Class A and Class B), and both A and B have a method called start(), which one does C use? Python uses MRO (Method Resolution Order), searching from left-to-right based on how you ordered the parents in the parentheses class C(A, B). Keep multiple inheritance structures simple to avoid confusing bugs.

Summary

  • Inheritance allows a child class to absorb all the attributes and methods of a parent class, preventing code duplication.
  • Overriding occurs when a child class redefines a parent’s method to give it custom, specialized behavior.
  • The super() function allows a child class to temporarily call a parent’s original method—most notably used to pass arguments up to the parent’s __init__ constructor.
  • Polymorphism is the ability to treat different class instances the exact same way in your code, trusting that each object knows how to execute its own version of an overridden method.
  • Python supports Multiple Inheritance, meaning a single class can inherit from multiple parent classes simultaneously.

Leave a Comment