- What is method overriding ā redefining parent class methods in child classes
- Why we need it ā customizing behavior for specific child classes
- How it works ā the mechanics of overriding
- super() method ā calling parent methods from child classes
- Constructor overriding ā working with __init__ in child classes
- Real-world use ā practical examples you can use
What is Method Overriding?
Method overriding is a feature in Python that allows a child class to redefine a method that it inherited from its parent class. When you override a method, you're telling Python: "I want this method to work differently for this specific child class."
Think of it like a family recipe. Your grandmother's recipe (parent class) is great, but you want to add your own twist to it. You still use the same basic ingredients and steps, but you change a few things to make it your own. That's exactly what method overriding is ā taking what you inherited and making it better suited for your needs.
Method overriding is a key part of polymorphism ā the ability to use the same method name across different classes, but each class implements it differently.
š” Key concept: Method overriding allows child classes to provide their own implementation of a method that's already defined in the parent class. It's like customizing something you inherited to fit your specific needs.
Why Do We Need Method Overriding?
Customizing Behavior for Each Child
Without method overriding, all child classes would have the exact same behavior. But in real life, different types need different behavior. A dog and a cat both make sounds, but they make different sounds. Method overriding lets you give each child its own unique behavior while still being part of the same family.
# Method overriding in action
class Animal:
"""Parent class - all animals share this"""
def __init__(self, name):
self.name = name
def make_sound(self):
return f"{self.name} makes a generic sound"
def move(self):
return f"{self.name} moves somehow"
def eat(self):
return f"{self.name} eats food"
# Without overriding ā all animals behave the same
class GenericAnimal(Animal):
pass
generic = GenericAnimal("Something")
print(generic.make_sound()) # Something makes a generic sound
# WITH overriding ā each animal has its own behavior
class Dog(Animal):
def make_sound(self):
return f"{self.name} says Woof!"
def move(self):
return f"{self.name} runs on four legs"
def eat(self):
return f"{self.name} eats dog food"
class Cat(Animal):
def make_sound(self):
return f"{self.name} says Meow!"
def move(self):
return f"{self.name} walks silently"
def eat(self):
return f"{self.name} eats cat food"
class Bird(Animal):
def make_sound(self):
return f"{self.name} says Chirp!"
def move(self):
return f"{self.name} flies in the sky"
def eat(self):
return f"{self.name} eats seeds"
# Using overridden methods
print("=== DOG ===")
dog = Dog("Buddy")
print(dog.make_sound()) # Buddy says Woof!
print(dog.move()) # Buddy runs on four legs
print(dog.eat()) # Buddy eats dog food
print("\n=== CAT ===")
cat = Cat("Whiskers")
print(cat.make_sound()) # Whiskers says Meow!
print(cat.move()) # Whiskers walks silently
print(cat.eat()) # Whiskers eats cat food
print("\n=== BIRD ===")
bird = Bird("Tweety")
print(bird.make_sound()) # Tweety says Chirp!
print(bird.move()) # Tweety flies in the sky
print(bird.eat()) # Tweety eats seeds
print("\nā
Method overriding gives each animal its own unique behavior!")
print("All animals have the same method names, but different implementations.")
Why override methods:
- Customization ā each child can behave differently
- Flexibility ā you can change behavior without changing parent class
- Polymorphism ā same method name, different implementations
- Real-world modeling ā different types have different behaviors
- Code reuse ā still inherits common methods, overrides only what's different
Quick Check: Why would you override a method in a child class? (Answer: To customize behavior for that specific child class)
How Method Overriding Works
Understanding the Mechanics
When you call a method on an object, Python looks for it in a specific order. It first checks the child class. If the method exists there, Python uses it ā that's method overriding in action. If it doesn't find it there, it goes up to the parent class.
This is called the method lookup chain. It's how Python decides which version of a method to use. The child's version always takes priority if it exists.
# How method overriding works
class Parent:
def greet(self):
return "Hello from Parent"
def farewell(self):
return "Goodbye from Parent"
def common(self):
return "This is common to everyone"
class Child(Parent):
# Overriding the greet method
def greet(self):
return "Hello from Child (overridden!)"
# Not overriding farewell ā uses parent's version
# Not overriding common ā uses parent's version
class Grandchild(Child):
# Overriding the greet method again
def greet(self):
return "Hello from Grandchild (overridden again!)"
# Overriding farewell too
def farewell(self):
return "Goodbye from Grandchild"
# Testing the lookup chain
print("=== PARENT OBJECT ===")
parent = Parent()
print(f"greet(): {parent.greet()}")
print(f"farewell(): {parent.farewell()}")
print(f"common(): {parent.common()}")
print("\n=== CHILD OBJECT ===")
child = Child()
print(f"greet(): {child.greet()}") # Child's version
print(f"farewell(): {child.farewell()}") # Parent's version (not overridden)
print(f"common(): {child.common()}") # Parent's version (not overridden)
print("\n=== GRANDCHILD OBJECT ===")
grandchild = Grandchild()
print(f"greet(): {grandchild.greet()}") # Grandchild's version
print(f"farewell(): {grandchild.farewell()}") # Grandchild's version (overridden)
print(f"common(): {grandchild.common()}") # Parent's version (not overridden)
print("\n=== LOOKUP CHAIN ===")
print("When grandchild.greet() is called:")
print("1. Python checks Grandchild class ā found it!")
print("2. Uses Grandchild's version")
print("\nWhen grandchild.farewell() is called:")
print("1. Python checks Grandchild class ā not found")
print("2. Checks Child class ā not found")
print("3. Checks Parent class ā found it!")
print("4. Uses Parent's version")
print("\nThe first matching method found in the chain is used.")
How method overriding works:
- Child first ā Python looks in the child class first
- Parent next ā if not found in child, looks in parent
- Continues up ā keeps going up the inheritance chain
- First match wins ā the first method found is used
- Overriding means replacing ā the child's version replaces the parent's
Quick Check: Where does Python first look for a method when called on an object? (Answer: In the object's class first, then up the inheritance chain)
Using super() to Call Parent Methods
Accessing the Parent's Version
Sometimes you want to extend the parent's behavior rather than completely replace it. That's where super() comes in. super() allows you to call the parent class's version of a method from inside the child class.
This is like saying: "Do what the parent does, but also do this extra stuff." It's very useful when you want to add to the parent's behavior rather than change it completely.
# Using super() to call parent methods
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
self.is_running = False
def start(self):
self.is_running = True
return f"Engine started for {self.brand} {self.model}"
def stop(self):
self.is_running = False
return f"Engine stopped for {self.brand} {self.model}"
def get_info(self):
return f"{self.brand} {self.model}"
# Child class that uses super() to extend parent behavior
class ElectricCar(Vehicle):
def __init__(self, brand, model, battery_capacity):
# Call parent __init__ first
super().__init__(brand, model)
self.battery_capacity = battery_capacity
self.charge_level = 100
def start(self):
# Call parent's start method first
parent_result = super().start()
# Then add electric car specific behavior
return f"{parent_result} with {self.charge_level}% battery"
def stop(self):
parent_result = super().stop()
return f"{parent_result} (electric motors disengaged)"
def get_info(self):
# Get parent's info and add battery info
parent_info = super().get_info()
return f"{parent_info} with {self.battery_capacity}kWh battery"
# Child class that uses super() with additional parameters
class SportsCar(Vehicle):
def __init__(self, brand, model, horse_power, top_speed):
super().__init__(brand, model)
self.horse_power = horse_power
self.top_speed = top_speed
self.sport_mode = False
def start(self):
parent_result = super().start()
self.sport_mode = True
return f"{parent_result} in sport mode! šļø"
def get_info(self):
return f"{super().get_info()} ({self.horse_power}HP, {self.top_speed}mph)"
# Using the classes
print("=== ELECTRIC CAR ===")
tesla = ElectricCar("Tesla", "Model 3", 75)
print(tesla.start()) # Engine started... with 100% battery
print(tesla.get_info()) # Tesla Model 3 with 75kWh battery
print(tesla.stop()) # Engine stopped (electric motors disengaged)
print("\n=== SPORTS CAR ===")
ferrari = SportsCar("Ferrari", "F8", 710, 211)
print(ferrari.start()) # Engine started... in sport mode!
print(ferrari.get_info()) # Ferrari F8 (710HP, 211mph)
print("\nā
super() lets you call the parent's version while adding your own!")
print("This is called 'extending' behavior rather than replacing it.")
super() key points:
- Extends behavior ā adds to parent's behavior instead of replacing it
- Maintains parent logic ā ensures parent's code still runs
- Constructor chaining ā essential for proper initialization
- Follows MRO ā works correctly with multiple inheritance
- Code reuse ā don't duplicate parent's code
Quick Check: When should you use super() in an overridden method? (Answer: When you want to extend the parent's behavior rather than completely replace it)
Overriding Constructors
Working with __init__ in Child Classes
The __init__ method (constructor) is just like any other method ā you can override it in child classes. When you do, it's very important to call the parent's __init__ using super().__init__(). Otherwise, the parent's attributes won't be initialized.
Think of it like building a house. The parent class lays the foundation and builds the walls. The child class adds the roof and finishes the interior. You need the foundation and walls first ā that's why you call the parent's constructor first.
# Overriding constructors
class Product:
"""Parent class - basic product"""
def __init__(self, product_id, name, price):
print(f"Product.__init__ called for {name}")
self.product_id = product_id
self.name = name
self.price = price
self.in_stock = True
def get_info(self):
return f"ID: {self.product_id}, Name: {self.name}, Price: ${self.price}"
# Good child class ā calls super().__init__()
class ElectronicProduct(Product):
def __init__(self, product_id, name, price, brand, warranty_years):
# MUST call parent's __init__ first
super().__init__(product_id, name, price)
print(f"ElectronicProduct.__init__ called for {name}")
self.brand = brand
self.warranty_years = warranty_years
def get_info(self):
return f"{super().get_info()}, Brand: {self.brand}, Warranty: {self.warranty_years} years"
# Bad child class ā forgets to call super().__init__()
class BrokenProduct(Product):
def __init__(self, product_id, name, price, discount):
# MISSING: super().__init__(product_id, name, price)
print(f"BrokenProduct.__init__ called for {name}")
self.discount = discount
# Good child class with extra logic
class BookProduct(Product):
def __init__(self, product_id, name, price, author, pages):
# Can do some validation before calling parent
if pages <= 0:
raise ValueError("Pages must be greater than 0")
# Then call parent
super().__init__(product_id, name, price)
print(f"BookProduct.__init__ called for {name}")
self.author = author
self.pages = pages
def get_info(self):
return f"{super().get_info()}, Author: {self.author}, Pages: {self.pages}"
# Using good classes
print("=== ELECTRONIC PRODUCT ===")
laptop = ElectronicProduct("E001", "Laptop", 999.99, "Dell", 2)
print(laptop.get_info())
print("\n=== BOOK PRODUCT ===")
book = BookProduct("B001", "Python Guide", 49.99, "John Smith", 350)
print(book.get_info())
# The broken class would cause an error:
# broken = BrokenProduct("B001", "Broken", 100, 10)
# print(broken.name) # AttributeError! name was never set
print("\nā
Always call super().__init__() in child classes!")
print("This ensures all parent attributes are properly initialized.")
Constructor overriding key points:
- Call super().__init__() ā always call the parent constructor
- Initialize parent first ā parent attributes must be set before child attributes
- Can add validation ā you can validate before calling parent
- Can add extra logic ā do additional setup after parent init
- Never forget super() ā forgetting causes AttributeError
Quick Check: What happens if you don't call super().__init__() in a child class? (Answer: Parent attributes won't be initialized, causing AttributeError)
Real-World Examples
Seeing Method Overriding in Action
# Real-world example: A Notification System
class Notification:
"""Parent class - basic notification"""
def __init__(self, recipient, message):
self.recipient = recipient
self.message = message
self.sent_at = None
def send(self):
"""Send the notification - generic version"""
self.sent_at = "now"
return f"Sending notification to {self.recipient}: {self.message}"
def get_status(self):
status = "Sent" if self.sent_at else "Pending"
return f"Status: {status} at {self.sent_at or 'not sent yet'}"
def format_message(self):
"""Format the message - can be overridden"""
return self.message
# Child class 1 - Email Notification
class EmailNotification(Notification):
def __init__(self, recipient, message, subject, cc=None):
# Override constructor - add email-specific attributes
super().__init__(recipient, message)
self.subject = subject
self.cc = cc or []
self.is_html = False
def send(self):
# Override send - add email-specific behavior
super().send() # Call parent's send to set sent_at
cc_text = f", CC: {self.cc}" if self.cc else ""
return f"š§ Sending email to {self.recipient}{cc_text}: {self.subject}"
def format_message(self):
# Override format - add HTML support
if self.is_html:
return f"{self.subject}
{self.message}
"
return f"SUBJECT: {self.subject}\n\n{self.message}"
def add_attachment(self, file_name):
"""Email-specific method"""
return f"Attached {file_name} to email"
# Child class 2 - SMS Notification
class SMSNotification(Notification):
def __init__(self, recipient, message, sender_id):
super().__init__(recipient, message)
self.sender_id = sender_id
self.message_limit = 160
self.is_sent = False
def send(self):
# Override send - add SMS-specific behavior
if len(self.message) > self.message_limit:
return f"ā SMS too long! {len(self.message)} chars (max {self.message_limit})"
self.sent_at = "now"
self.is_sent = True
return f"š± Sending SMS from {self.sender_id} to {self.recipient}"
def format_message(self):
# Override format - truncate for SMS
if len(self.message) > self.message_limit:
return self.message[:self.message_limit] + "..."
return self.message
def get_delivery_report(self):
"""SMS-specific method"""
return f"Delivery report: {'Delivered' if self.is_sent else 'Pending'}"
# Child class 3 - Push Notification
class PushNotification(Notification):
def __init__(self, recipient, message, app_name, priority="normal"):
super().__init__(recipient, message)
self.app_name = app_name
self.priority = priority
self.actions = []
def send(self):
# Override send - add push notification behavior
super().send() # Call parent's send
actions_text = f" with actions: {self.actions}" if self.actions else ""
return f"š² Sending push notification from {self.app_name} to {self.recipient}{actions_text} (Priority: {self.priority})"
def format_message(self):
# Override format - add priority indicator
priority_emoji = "š“" if self.priority == "high" else "š¢"
return f"{priority_emoji} {self.message}"
def add_action(self, action_name):
"""Push notification-specific method"""
self.actions.append(action_name)
return f"Added action: {action_name}"
# Using the notification system
print("=" * 50)
print("NOTIFICATION SYSTEM WITH METHOD OVERRIDING")
print("=" * 50)
def send_notification(notification):
"""Function that works with any notification type"""
print(notification.send())
print(f"Formatted: {notification.format_message()}")
print(notification.get_status())
print("\n=== EMAIL NOTIFICATION ===")
email = EmailNotification(
"user@example.com",
"Hello! This is a test message.",
"Test Email",
cc=["boss@example.com"]
)
email.is_html = True
send_notification(email)
print(email.add_attachment("report.pdf"))
print("\n=== SMS NOTIFICATION ===")
sms = SMSNotification("+1234567890", "Your code is 123456", "MyApp")
send_notification(sms)
print(sms.get_delivery_report())
print("\n=== PUSH NOTIFICATION ===")
push = PushNotification("user123", "New message from Alice!", "MyApp", "high")
push.add_action("Reply")
push.add_action("Delete")
send_notification(push)
print("\nā
Each notification type has its own behavior!")
print("The send_notification function works with ALL notification types.")
Real-world example key points:
- Parent class ā Notification defines common interface (send, format_message, get_status)
- EmailNotification ā adds subject, CC, HTML support, attachments
- SMSNotification ā adds character limit, sender_id, delivery reports
- PushNotification ā adds app_name, priority, actions
- Polymorphism ā send_notification() works with any notification type
Quick Check: What does the send_notification function demonstrate? (Answer: Polymorphism ā it works with any class that inherits from Notification)
Best Practices for Method Overriding
Using Method Overriding Effectively
# Best practices for method overriding
# 1. Always call super().__init__() in child class constructors
class Base:
def __init__(self, value):
self.value = value
class Child(Base):
def __init__(self, value, extra):
super().__init__(value) # ā
Good
self.extra = extra
# 2. Keep the method signature consistent
# Good ā same parameters
class Parent:
def process(self, data):
return f"Processing {data}"
class Child(Parent):
def process(self, data):
return f"Child processing {data}"
# Bad ā different parameters (can cause confusion)
class BadChild(Parent):
def process(self, data, extra): # ā ļø Different signature
return f"Processing {data} with {extra}"
# 3. Use super() to extend, not just replace
class Worker:
def work(self):
return "Working hard"
class Manager(Worker):
def work(self):
return f"{super().work()} and managing team" # ā
Extends
# Bad ā completely replaces without using super
class BadManager(Worker):
def work(self):
return "Managing team" # ā Loses "Working hard"
# 4. Document why you're overriding
class Animal:
def speak(self):
"""Return the animal's sound"""
return "Generic sound"
class Dog(Animal):
def speak(self):
"""Return the dog's sound ā overridden to be specific"""
return "Woof!"
# 5. Don't override methods that don't need it
# Only override when you need different behavior
# 6. Test overridden methods
class Tester:
def test(self):
return "Testing..."
class TestSub(Tester):
def test(self):
result = super().test()
return f"Extended: {result}"
# 7. Be careful with private methods
class Private:
def __method(self):
return "Private method" # Name mangling makes this hard to override
class BadPrivate(Private):
def __method(self):
return "Trying to override" # This won't work properly
Best practices summary:
- Call super().__init__() ā always in child class constructors
- Keep signatures consistent ā same parameters as parent
- Extend, don't replace ā use super() to add to parent behavior
- Document your overrides ā explain why you're overriding
- Only override when needed ā don't override unnecessarily
- Test your overrides ā make sure they work correctly
- Avoid private method overrides ā name mangling makes it tricky
Quick Check: Should you always keep the same method signature when overriding? (Answer: Yes, to maintain consistency and avoid confusion)
Try It Yourself
Experiment with method overriding in the editor below.
METHOD OVERRIDING PRACTICE
========================================
1. CREATING A PARENT CLASS
2. CREATING CHILD CLASSES
3. USING THE CLASSES
MICROWAVE:
Samsung microwave is heating at 1000W
š Heating food in Samsung microwave
Samsung MG-101: ON
COFFEE MAKER:
Breville coffee maker is brewing 4 cups
ā Brewed coffee (total: 1)
ā Brewed coffee (total: 2)
Breville Barista: ON
BLENDER:
Vitamix blender is blending at speed 6
š„¤ Blending smoothie in Vitamix blender
Vitamix Pro-750: ON
4. POLYMORPHISM IN ACTION
Samsung: š Heating food in Samsung microwave
Breville: ā Brewed coffee (total: 3)
Vitamix: š„¤ Blending smoothie in Vitamix blender
Method overriding practice complete!
You've Got It!
You now understand method overriding in Python. You know how to redefine parent methods in child classes, use super() to extend behavior, and override constructors properly.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What's the difference between method overriding and method overloading?
Do I have to call super() when overriding a method?
Can I override a method in a grandchild class?
What happens if I change the method signature when overriding?
What's a common interview question about method overriding?
When should I use method overriding vs composition?
Where to Go From Here
Now that you understand method overriding, check out these related topics:
Overloading vs Overriding
Learn the key differences between these two concepts.
Learn More āAbstract Methods
Learn about methods that must be implemented by child classes.
Learn More āPolymorphism
Learn how overriding enables polymorphic behavior.
Learn More ā