Loading learning content...
Inheritance gives subclasses access to the attributes and methods of their parent classes. But what happens when a subclass needs a method to behave differently than the version it inherited? This is where method overriding becomes essential—one of the most powerful mechanisms in object-oriented programming that enables specialization while maintaining polymorphic behavior.
Method overriding allows a subclass to provide its own specific implementation of a method that is already defined in its parent class. Rather than being forced to accept inherited behavior as-is, subclasses can redefine methods to suit their specialized needs while still conforming to the interface established by the parent.
By the end of this page, you will understand what method overriding is, why it is fundamental to polymorphism, how it differs from method hiding, and when overriding is the appropriate design choice. You'll see how overriding enables specialized behavior within a consistent inheritance hierarchy.
Method overriding occurs when a subclass declares a method with the same signature (name, parameters, and in most languages, compatible return type) as a method in its parent class. When this happens, the subclass's version of the method replaces the parent's version for instances of that subclass.
Consider a simple example: a Shape class with a draw() method. Every subclass—Circle, Rectangle, Triangle—needs a draw() method, but each must draw something entirely different. The parent class defines the interface (the promise that all shapes can be drawn), while each subclass provides its own implementation (the specific drawing logic).
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
class Shape: """Base class defining the shape interface.""" def draw(self): """Draw the shape. Subclasses must override this.""" print("Drawing a generic shape") def get_area(self): """Calculate area. Subclasses should override.""" return 0 class Circle(Shape): """Circle specialization of Shape.""" def __init__(self, radius): self.radius = radius def draw(self): """Override: Draw a circle specifically.""" print(f"Drawing a circle with radius {self.radius}") def get_area(self): """Override: Calculate circle area.""" import math return math.pi * self.radius ** 2 class Rectangle(Shape): """Rectangle specialization of Shape.""" def __init__(self, width, height): self.width = width self.height = height def draw(self): """Override: Draw a rectangle specifically.""" print(f"Drawing a {self.width}x{self.height} rectangle") def get_area(self): """Override: Calculate rectangle area.""" return self.width * self.height # Polymorphism in actionshapes = [Circle(5), Rectangle(4, 6), Shape()] for shape in shapes: shape.draw() # Each calls its own overridden version print(f"Area: {shape.get_area()}")In this example, Circle and Rectangle both override the draw() and get_area() methods from Shape. When we iterate over a collection of shapes and call draw(), each shape executes its own version—the Circle draws a circle, the Rectangle draws a rectangle. This is runtime polymorphism in action: the same method call produces different behavior depending on the actual type of the object.
Method overriding serves several critical purposes in object-oriented design, each addressing a fundamental need in building flexible, maintainable software systems:
SavingsAccount calculates interest differently than a CheckingAccount, even though both are BankAccount types. Overriding enables each to implement its own interest calculation while sharing the common account interface.SavingsAccount can be used anywhere a BankAccount is expected, and it will behave appropriately.super.Method overriding promotes maintainability through localized changes. If Circle's drawing logic needs to change, you modify only Circle.draw(). Client code using Shape references doesn't need to know or care—it continues calling draw() and gets the updated behavior automatically.
One of the most common sources of confusion for developers learning OOP is the distinction between method overriding and method overloading. These terms sound similar but represent fundamentally different concepts:
Method Overriding: A subclass provides a new implementation for a method with the exact same signature that exists in the parent class. This is resolved at runtime based on the actual object type.
Method Overloading: Multiple methods in the same class (or a subclass) share the same name but have different parameter lists. This is resolved at compile time based on the method signature.
| Aspect | Overriding | Overloading |
|---|---|---|
| Definition | Replacing parent method implementation | Multiple methods with same name, different parameters |
| Inheritance | Requires inheritance (parent-child) | Same class or inheritance hierarchy |
| Method Signature | Must be identical (or covariant return) | Must differ in parameter types or count |
| Resolution Time | Runtime (dynamic dispatch) | Compile time (static binding) |
| Polymorphism Type | Runtime polymorphism | Compile-time polymorphism |
| Access Modifiers | Cannot be more restrictive | Can have any access modifier |
| Return Type | Same or covariant (more specific) | Can be different (not part of signature) |
| Purpose | Specialization of behavior | Convenience for different parameter types |
123456789101112131415161718192021222324252627282930313233343536373839404142434445
class Calculator { // Method OVERLOADING: same name, different parameters public int add(int a, int b) { return a + b; } public double add(double a, double b) { return a + b; } public int add(int a, int b, int c) { return a + b + c; }} class ScientificCalculator extends Calculator { // Method OVERRIDING: same signature as parent's add(int, int) @Override public int add(int a, int b) { System.out.println("ScientificCalculator adding..."); return super.add(a, b); // Can still use parent's implementation } // This is OVERLOADING in the subclass, not overriding public double add(double a, double b, double c) { return a + b + c; }} public class Demo { public static void main(String[] args) { Calculator calc = new ScientificCalculator(); // Overriding: resolved at runtime, uses ScientificCalculator's version int result1 = calc.add(5, 3); // Prints "ScientificCalculator adding..." // Overloading: resolved at compile time based on argument types double result2 = calc.add(5.0, 3.0); // Uses Calculator's double version System.out.println(result1); // 8 System.out.println(result2); // 8.0 }}A frequent bug occurs when developers intend to override a method but accidentally change the parameter types, creating an overload instead. The parent's method continues to be called when expected to be overridden. In Java, using the @Override annotation catches this at compile time. In Python, tools like mypy can detect signature mismatches.
Another important distinction is between method overriding and method hiding. Method overriding applies to instance methods—methods that operate on object instances. But what happens with static methods in a class hierarchy?
Static methods belong to the class itself, not to instances. When a subclass defines a static method with the same signature as a static method in its parent, this is called method hiding, not overriding. The crucial difference lies in how the method is resolved:
parent.method() on a child object invokes child's version@Override annotation appliesParent.method() always uses Parent's version@Override annotation does NOT apply1234567891011121314151617181920212223242526272829303132333435363738394041
class Animal { // Instance method - can be overridden public void speak() { System.out.println("Animal makes a sound"); } // Static method - can only be hidden, not overridden public static void describe() { System.out.println("I am an Animal"); }} class Dog extends Animal { // OVERRIDES the instance method @Override public void speak() { System.out.println("Dog barks"); } // HIDES the static method (NOT overriding!) public static void describe() { System.out.println("I am a Dog"); }} public class Demo { public static void main(String[] args) { Animal animal = new Dog(); // Reference is Animal, object is Dog // Instance method: OVERRIDING - uses runtime type (Dog) animal.speak(); // Output: "Dog barks" // Static method: HIDING - uses compile-time reference type (Animal) animal.describe(); // Output: "I am an Animal" <-- Not "I am a Dog"! // To get Dog's static version, must use Dog reference Dog.describe(); // Output: "I am a Dog" }}Method hiding is generally considered a code smell because it breaks the intuitive expectation of polymorphic behavior. If you need different static behavior per subclass, consider whether the method should be an instance method instead, or whether a different design approach (like factory methods) would be clearer.
Understanding how method overriding works at a mechanical level helps clarify why it behaves as it does. When you call an instance method on an object, the runtime system performs a process called dynamic dispatch (or late binding):
This process happens transparently every time you call a method on an object reference, enabling polymorphic behavior.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
class Vehicle: def start(self): print("Vehicle starting...") def move(self): print("Vehicle moving in some way") def stop(self): print("Vehicle stopping...") class Car(Vehicle): def move(self): # Override: Car has specific movement print("Car driving on wheels") class Boat(Vehicle): def move(self): # Override: Boat has specific movement print("Boat sailing on water") class Hovercraft(Vehicle): def move(self): # Override: Hovercraft has specific movement print("Hovercraft gliding on air cushion") def travel(vehicle: Vehicle): """ This function works with any Vehicle. The actual move() behavior depends on the runtime type. This is the power of method overriding + polymorphism. """ vehicle.start() # Uses Vehicle.start() (not overridden) vehicle.move() # Uses the OVERRIDDEN version for each type vehicle.stop() # Uses Vehicle.stop() (not overridden) # Same code, different behaviorstravel(Car()) # "Car driving on wheels"travel(Boat()) # "Boat sailing on water"travel(Hovercraft()) # "Hovercraft gliding on air cushion"In this example, the travel() function doesn't know or care about the specific type of vehicle it receives. It calls move(), and the runtime system ensures the correct overridden version executes. This decoupling—where client code depends only on the parent type but gets specialized behavior—is a cornerstone of extensible software design.
Method overriding is a powerful tool, but like all tools, it should be applied thoughtfully. Here are guidelines for when overriding is appropriate:
PremiumUser needs different discount logic than RegularUser, but both are User typesReadOnlyFile should override write() to refuse writes, while maintaining file interface compatibilityvirtual, abstract, or documented as extension points are meant to be overriddensuper or extract common logicAsk yourself: 'Does my subclass represent a genuine specialization that should behave differently in this specific way?' If yes, override. If you're using overriding to work around a poor inheritance design, reconsider the class hierarchy itself.
We've established the foundational understanding of method overriding—one of the most powerful mechanisms in object-oriented programming. Let's consolidate the key concepts:
What's next:
Now that we understand what method overriding is and why it matters, the next page explores the rules and constraints that govern overriding. Not just any method can be overridden, and not in any way—understanding these rules is essential for writing correct, maintainable inheritance hierarchies.
You now understand the concept of method overriding, its relationship to polymorphism, and how it differs from overloading and method hiding. Next, we'll examine the specific rules that control when and how methods can be overridden.