🔄 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.
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?
Track Your Learning Progress
Sign in to bookmark tutorials and keep track of your learning journey.
Your progress is saved automatically as you read.