🔄 Method Overriding

Method overriding allows child classes to provide specialized implementations of methods inherited from parent classes. This powerful feature enables you to customize behavior while maintaining the same interface, creating flexible hierarchies where each class can have its own version of common operations.

Think of method overriding like customizing a family recipe - you inherit the basic recipe from your parent, but you can modify ingredients or techniques to create your own signature version while keeping the same dish name.

# Method overriding in action
class Payment:
    def __init__(self, amount):
        self.amount = amount
    
    def process(self):
        return f"Processing ${self.amount} payment"
    
    def get_fee(self):
        return self.amount * 0.03  # 3% default fee

class CreditCard(Payment):
    def process(self):  # Override process method
        return f"Processing ${self.amount} via credit card"
    
    def get_fee(self):  # Override fee calculation
        return self.amount * 0.025  # 2.5% for credit cards

class PayPal(Payment):
    def process(self):  # Override process method
        return f"Processing ${self.amount} via PayPal"
    
    def get_fee(self):  # Override fee calculation
        return self.amount * 0.035 + 0.30  # 3.5% + $0.30

# Each payment type processes differently
credit = CreditCard(100)
paypal = PayPal(100)

print(credit.process())  # Credit card version
print(f"Fee: ${credit.get_fee()}")
print(paypal.process())  # PayPal version
print(f"Fee: ${paypal.get_fee():.2f}")

🎯 Understanding Method Overriding

Method overriding occurs when a child class defines a method with the same name and signature as a method in its parent class. The child's version replaces the parent's version for instances of the child class.

Basic Method Overriding

When you define a method in a child class with the same name as a parent method, the child version automatically takes precedence.

class Document:
    def __init__(self, title):
        self.title = title
    
    def save(self):
        return f"Saving document: {self.title}"
    
    def get_info(self):
        return f"Document: {self.title}"

class PDFDocument(Document):
    def save(self):  # Override parent save method
        return f"Saving PDF: {self.title} with compression"
    
    def get_info(self):  # Override parent get_info method
        return f"PDF Document: {self.title} (Portable Format)"

class WordDocument(Document):
    def save(self):  # Override parent save method
        return f"Saving Word document: {self.title} with auto-save"

# Each document type saves differently
pdf = PDFDocument("Python Guide")
word = WordDocument("Meeting Notes")

print(pdf.save())     # Uses PDF-specific save
print(pdf.get_info()) # Uses PDF-specific info
print(word.save())    # Uses Word-specific save
print(word.get_info()) # Uses inherited method (not overridden)

Method Signature Matching

For overriding to work correctly, the child method must have the same name and compatible parameter signature as the parent method.

class Calculator:
    def calculate(self, operation, a, b):
        if operation == "add":
            return a + b
        return 0
    
    def display_result(self, result):
        return f"Result: {result}"

class ScientificCalculator(Calculator):
    def calculate(self, operation, a, b):  # Correct override - same signature
        if operation == "add":
            return a + b
        elif operation == "power":
            return a ** b
        elif operation == "sqrt":
            return a ** 0.5
        return 0
    
    def display_result(self, result):  # Correct override
        return f"Scientific Result: {result:.4f}"
    
    def calculate_advanced(self, operation, *args):  # New method - different signature
        if operation == "factorial" and len(args) == 1:
            n = args[0]
            result = 1
            for i in range(1, n + 1):
                result *= i
            return result
        return 0

calc = ScientificCalculator()
print(calc.calculate("power", 2, 3))  # Uses overridden method
print(calc.display_result(8.0000))    # Uses overridden display
print(calc.calculate_advanced("factorial", 5))  # Uses new method

⚡ Using super() in Overridden Methods

The super() function allows you to access parent methods from overridden child methods, enabling you to extend rather than completely replace parent functionality.

Extending Parent Behavior

Using super() lets you call the parent method and then add additional functionality, combining inherited behavior with specialized features.

class Logger:
    def __init__(self, name):
        self.name = name
        self.messages = []
    
    def log(self, message):
        self.messages.append(message)
        return f"Logged: {message}"

class TimestampLogger(Logger):
    def __init__(self, name):
        super().__init__(name)  # Call parent constructor
        import datetime
        self.created = datetime.datetime.now()
    
    def log(self, message):
        # Extend parent behavior
        import datetime
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        timestamped_message = f"[{timestamp}] {message}"
        
        # Call parent method with modified message
        return super().log(timestamped_message)

class FileLogger(TimestampLogger):
    def __init__(self, name, filename):
        super().__init__(name)  # Call parent constructor
        self.filename = filename
    
    def log(self, message):
        # Further extend behavior
        result = super().log(message)  # Gets timestamped message
        # Simulate file writing
        file_msg = f"Written to {self.filename}: {message}"
        return f"{result} | {file_msg}"

# Layered behavior extension
file_logger = FileLogger("AppLogger", "app.log")
print(file_logger.log("User logged in"))
print(f"Messages stored: {len(file_logger.messages)}")

Conditional Parent Calls

Sometimes you want to call the parent method only under certain conditions, giving you fine-grained control over when inherited behavior applies.

class Validator:
    def validate(self, data):
        if not data:
            return False, "Data is required"
        return True, "Valid"

class EmailValidator(Validator):
    def validate(self, data):
        # First check basic validation
        is_valid, message = super().validate(data)
        if not is_valid:
            return is_valid, message
        
        # Add email-specific validation
        if "@" not in data:
            return False, "Email must contain @"
        if "." not in data.split("@")[1]:
            return False, "Email domain must contain ."
        
        return True, "Valid email"

class StrictEmailValidator(EmailValidator):
    def __init__(self, allowed_domains):
        self.allowed_domains = allowed_domains
    
    def validate(self, data):
        # Call parent validation first
        is_valid, message = super().validate(data)
        if not is_valid:
            return is_valid, message
        
        # Add domain restriction
        domain = data.split("@")[1]
        if domain not in self.allowed_domains:
            return False, f"Domain {domain} not allowed"
        
        return True, "Valid email from approved domain"

# Layered validation
basic = Validator()
email = EmailValidator()
strict = StrictEmailValidator(["company.com", "partner.org"])

test_data = ["", "invalid", "test@example.com", "user@company.com"]

for data in test_data:
    print(f"Data: '{data}'")
    print(f"  Basic: {basic.validate(data)}")
    print(f"  Email: {email.validate(data)}")
    print(f"  Strict: {strict.validate(data)}")

🚀 Polymorphism Through Overriding

Method overriding enables polymorphism - the ability to use different classes interchangeably through a common interface.

Polymorphic Behavior

Different classes can respond to the same method call with their own specialized behavior.

class MediaPlayer:
    def __init__(self, filename):
        self.filename = filename
    
    def play(self):
        return f"Playing {self.filename}"
    
    def stop(self):
        return f"Stopping {self.filename}"

class AudioPlayer(MediaPlayer):
    def play(self):
        return f"🎵 Playing audio: {self.filename}"
    
    def adjust_volume(self, level):
        return f"Volume set to {level}%"

class VideoPlayer(MediaPlayer):
    def play(self):
        return f"🎬 Playing video: {self.filename}"
    
    def set_quality(self, quality):
        return f"Quality set to {quality}"

class StreamPlayer(MediaPlayer):
    def play(self):
        return f"📡 Streaming: {self.filename}"
    
    def check_connection(self):
        return "Connection stable"

# Polymorphic usage - same interface, different behaviors
players = [
    AudioPlayer("song.mp3"),
    VideoPlayer("movie.mp4"),
    StreamPlayer("live_stream")
]

for player in players:
    print(player.play())  # Each player type responds differently
    print(player.stop())  # Inherited method works for all

🌟 Advanced Overriding Patterns

Template Method Pattern

Define a skeleton algorithm in the parent class, allowing subclasses to override specific steps.

class DataExporter:
    def export(self, data):
        # Template method - defines the process
        formatted_data = self.format_data(data)
        output = self.generate_output(formatted_data)
        result = self.save_output(output)
        return result
    
    def format_data(self, data):
        # Default implementation
        return str(data)
    
    def generate_output(self, formatted_data):
        # Must be overridden by subclasses
        raise NotImplementedError("Subclasses must implement generate_output")
    
    def save_output(self, output):
        # Default implementation
        return f"Saved {len(output)} characters"

class JSONExporter(DataExporter):
    def format_data(self, data):
        # Override to format as JSON
        import json
        return json.dumps(data, indent=2)
    
    def generate_output(self, formatted_data):
        return f"JSON Export:\n{formatted_data}"

class CSVExporter(DataExporter):
    def format_data(self, data):
        # Override to format as CSV
        if isinstance(data, list) and isinstance(data[0], dict):
            headers = ",".join(data[0].keys())
            rows = [",".join(str(item[key]) for key in item.keys()) for item in data]
            return f"{headers}\n" + "\n".join(rows)
        return str(data)
    
    def generate_output(self, formatted_data):
        return f"CSV Export:\n{formatted_data}"

# Template pattern in action
data = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]

json_exporter = JSONExporter()
csv_exporter = CSVExporter()

print(json_exporter.export(data))
print("\n" + "="*40 + "\n")
print(csv_exporter.export(data))

Hands-on Exercise

Create a simple Employee system using method overriding. Make a base Employee class with calculate_pay() method, then create FullTimeEmployee and PartTimeEmployee classes that override this method with different calculations.

python
class Employee:
    def __init__(self, name, base_salary):
        # TODO: Set name and base_salary attributes
        pass
    
    def calculate_pay(self):
        # TODO: Base pay calculation (can be generic)
        pass
    
    def get_info(self):
        # TODO: Return employee information
        pass

class FullTimeEmployee(Employee):
    def __init__(self, name, base_salary):
        # TODO: Call parent constructor
        pass
    
    def calculate_pay(self):
        # TODO: Override to return full salary
        pass

class PartTimeEmployee(Employee):
    def __init__(self, name, hourly_rate, hours_worked):
        # TODO: Call parent constructor and set additional attributes
        pass
    
    def calculate_pay(self):
        # TODO: Override to calculate hourly pay
        pass

# TODO: Test your classes
full_time = FullTimeEmployee("Alice", 5000)
part_time = PartTimeEmployee("Bob", 20, 25)

print(full_time.get_info())
print(f"Pay: ${full_time.calculate_pay()}")
print(part_time.get_info())
print(f"Pay: ${part_time.calculate_pay()}")

Solution and Explanation 💡

Click to see the complete solution
class Employee:
    def __init__(self, name, base_salary):
        # Set name and base_salary attributes
        self.name = name
        self.base_salary = base_salary
    
    def calculate_pay(self):
        # Base pay calculation (can be generic)
        return self.base_salary
    
    def get_info(self):
        # Return employee information
        return f"Employee: {self.name}"

class FullTimeEmployee(Employee):
    def __init__(self, name, base_salary):
        # Call parent constructor
        super().__init__(name, base_salary)
    
    def calculate_pay(self):
        # Override to return full salary
        return self.base_salary

class PartTimeEmployee(Employee):
    def __init__(self, name, hourly_rate, hours_worked):
        # Call parent constructor and set additional attributes
        super().__init__(name, 0)  # Base salary not used for part-time
        self.hourly_rate = hourly_rate
        self.hours_worked = hours_worked
    
    def calculate_pay(self):
        # Override to calculate hourly pay
        return self.hourly_rate * self.hours_worked

# Test your classes
full_time = FullTimeEmployee("Alice", 5000)
part_time = PartTimeEmployee("Bob", 20, 25)

print(full_time.get_info())
print(f"Pay: ${full_time.calculate_pay()}")
print(part_time.get_info())
print(f"Pay: ${part_time.calculate_pay()}")

Key Learning Points:

  • 📌 Method overriding: Child classes provide their own implementation of parent methods
  • 📌 Polymorphism: Same method name (calculate_pay()) behaves differently in each class
  • 📌 super() usage: Call parent constructor to initialize common attributes
  • 📌 Specialized behavior: Each employee type calculates pay differently while maintaining same interface
  • 📌 Code reuse: Common functionality (like get_info()) stays in parent class

Learn more about variable visibility to control access to your class attributes and methods.

Test Your Knowledge

Test what you've learned about method overriding:

What's Next?

Now that you understand method overriding, you're ready to learn about variable visibility. This concept helps you control access to class attributes and methods, creating well-encapsulated objects with clear public and private interfaces.

Ready to continue? Check out our lesson on Variable Visibility.

Was this helpful?

😔Poor
🙁Fair
😊Good
😄Great
🤩Excellent