Method overriding was the concept that made object-oriented programming actually click for me. Before I understood it properly, inheritance felt like a way to avoid retyping code. After I understood it, I realized it’s really about letting a subclass customize behavior while still being treated as an instance of its parent type everywhere that matters. This guide walks through everything I’ve learned about overriding methods in Python — from the basic syntax to the internal mechanics of how Python actually decides which method gets called.
What Method Overriding Actually Means
Method overriding happens when a subclass defines a method with the same name as one already defined in its parent class, replacing (or extending) that behavior for instances of the subclass. It’s different from method overloading (which many other languages support and Python doesn’t, at least not in the traditional sense) — overriding is about redefining existing behavior down the inheritance chain, not defining multiple versions of a method with different argument signatures.
class Animal:
def speak(self):
return "Some generic animal sound"
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
animals = [Animal(), Dog(), Cat()]
for animal in animals:
print(animal.speak())
Output:
Some generic animal sound
Woof!
Meow!
Each subclass provides its own version of speak(), and calling .speak() on each object invokes the version appropriate to its actual class — this behavior, where the correct method is selected based on the object’s real type, is called polymorphism, and method overriding is the primary mechanism that makes it work.
The Method Resolution Order (MRO)
Understanding how Python decides which method to call is where this stops being magic. Every Python class has a Method Resolution Order — a defined sequence of classes Python searches, in order, when looking up an attribute or method. I can inspect it directly:
class Animal:
def speak(self):
return "Generic sound"
class Dog(Animal):
def speak(self):
return "Woof!"
print(Dog.__mro__)
Output:
(<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)
When I call dog_instance.speak(), Python walks this tuple left to right, checking each class for a speak attribute, and uses the first one it finds. Since Dog defines its own speak(), that’s found first and Animal.speak() is never reached — this is the entire mechanism behind overriding. For simple single-inheritance hierarchies like this one, the MRO is just the chain of parent classes in order. For multiple inheritance, Python computes it using an algorithm called C3 linearization, which guarantees a consistent, predictable order even across complex class hierarchies.
Calling the Parent’s Version with super()
Overriding doesn’t have to mean completely replacing the parent’s behavior — often I want to extend it, running the parent’s version and then adding something extra. super() is how I reach back up the MRO to call the next class’s version of a method.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_details(self):
return f"Name: {self.name}, Salary: {self.salary}"
class Manager(Employee):
def __init__(self, name, salary, team_size):
super().__init__(name, salary) # let the parent handle name/salary setup
self.team_size = team_size
def get_details(self):
base_details = super().get_details() # get the parent's formatted string
return f"{base_details}, Team Size: {self.team_size}"
manager = Manager("Alice", 95000, 8)
print(manager.get_details())
Output:
Name: Alice, Salary: 95000, Team Size: 8
This pattern — call super(), then add subclass-specific behavior — is by far the most common way I actually override methods in real code. Completely replacing a parent method without calling super() at all is less common in practice, usually reserved for cases where the subclass’s behavior is genuinely unrelated to the parent’s implementation.
Overriding init Specifically
__init__ deserves special mention because it’s the method I override most often, and forgetting super().__init__() here is one of the most common bugs I’ve both made and seen others make.
class Vehicle:
def __init__(self, make, model):
self.make = make
self.model = model
self.is_running = False
def start(self):
self.is_running = True
print(f"{self.make} {self.model} started")
class ElectricVehicle(Vehicle):
def __init__(self, make, model, battery_capacity):
super().__init__(make, model) # without this, self.make/self.model never get set
self.battery_capacity = battery_capacity
ev = ElectricVehicle("Tesla", "Model 3", 75)
ev.start()
print(f"Battery: {ev.battery_capacity} kWh")
If I forget super().__init__(make, model) here, self.make and self.model simply never get created on the instance, and calling ev.start() — which is inherited unchanged from Vehicle and references self.make — raises an AttributeError the moment it tries to use an attribute that was never set.
Overriding Built-in Dunder Methods
Some of the most useful overriding I do involves Python’s special “dunder” (double-underscore) methods, which control how built-in operations behave on custom objects.
class Money:
def __init__(self, amount, currency='USD'):
self.amount = amount
self.currency = currency
def __str__(self):
return f"{self.amount:.2f} {self.currency}"
def __repr__(self):
return f"Money({self.amount}, '{self.currency}')"
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.amount == other.amount and self.currency == other.currency
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("Cannot add different currencies")
return Money(self.amount + other.amount, self.currency)
price1 = Money(19.99)
price2 = Money(5.01)
print(price1) # uses __str__
print(price1 + price2) # uses __add__, then __str__ on the result
print(price1 == Money(19.99)) # uses __eq__
Output:
19.99 USD
25.00 USD
True
Overriding __eq__, __add__, __str__, and similar dunder methods is technically still method overriding — every class implicitly inherits from object, which already defines default (if unhelpful) versions of these methods, and I’m replacing that default behavior for my own class.
Extending Behavior Instead of Fully Replacing It
A pattern I use constantly: override a method, do some validation or logging, and then delegate to the parent’s original implementation for the actual work.
class Logger:
def log(self, message):
print(f"[LOG] {message}")
class TimestampedLogger(Logger):
def log(self, message):
import datetime
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
super().log(f"[{timestamp}] {message}")
logger = TimestampedLogger()
logger.log("Application started")
Output:
[LOG] [2026-07-30 10:15:32] Application started
Internal Working: How Attribute Lookup Actually Finds the Overridden Method
When I write instance.method_name(), Python’s attribute lookup mechanism first checks the instance’s own __dict__ for method_name (this is where instance attributes set via self.x = ... live, but generally not methods, unless explicitly assigned there). If not found there, it proceeds through the class’s __mro__ in order, checking each class’s own __dict__ for the attribute, and returns the first match — this is exactly the mechanism I demonstrated with Dog.__mro__ earlier.
Because this lookup happens at call time, not at class-definition time, the behavior is fully dynamic — if a subclass’s method is reassigned or monkey-patched after the class is defined, the new version is what gets found on the next call, since Python re-walks the MRO every single time the attribute is accessed rather than caching a fixed binding.
Overriding in Multiple Inheritance
Things get more interesting with multiple inheritance, where the C3 linearization algorithm determines a single, consistent MRO across multiple parent classes.
class Flyable:
def move(self):
return "Flying through the air"
class Swimmable:
def move(self):
return "Swimming through water"
class Duck(Flyable, Swimmable):
pass
duck = Duck()
print(duck.move())
print(Duck.__mro__)
Output:
Flying through the air
(<class '__main__.Duck'>, <class '__main__.Flyable'>, <class '__main__.Swimmable'>, <class 'object'>)
Since Duck inherits from Flyable first, and neither Duck nor Flyable define anything unusual, Flyable.move() wins because it appears before Swimmable in the MRO. If I wanted Duck to genuinely combine both behaviors, I’d override move() in Duck itself and call both parent versions explicitly, since implicit MRO resolution only picks one.
class Duck(Flyable, Swimmable):
def move(self):
return f"{Flyable.move(self)}, then {Swimmable.move(self)}"
duck = Duck()
print(duck.move())
Common Mistakes I’ve Made
- Forgetting
super().__init__(), leaving parent-class attributes never initialized and causingAttributeErrorlater, often far from the actual bug’s source. - Overriding
__eq__without also overriding__hash__, which silently makes instances unhashable in ways that only surface later, when trying to put them in a set or use them as a dict key. - Assuming method overriding works like overloading — Python doesn’t support multiple same-named methods differing only by argument signature; the last one defined simply replaces all previous ones.
- Not calling
super()when extending behavior was actually the intent, accidentally dropping important parent-class logic (like validation) entirely. - Misreading the MRO in multiple inheritance, assuming a specific parent’s method will run when actually a different parent earlier in the MRO provides it instead.
Real-World Use Cases
- Framework customization — overriding lifecycle methods in web frameworks (like Django’s
save()on a model, or a customclean()validation method). - Custom exception classes overriding
__init__and__str__to provide richer error messages. - Plugin/strategy architectures where a base class defines an interface and subclasses override specific methods to provide different behaviors.
- Testing — overriding methods in mock/stub subclasses to isolate behavior during unit tests.
FAQs
What’s the difference between method overriding and method overloading? Overriding redefines an inherited method in a subclass. Overloading — having multiple methods with the same name but different parameters — isn’t natively supported in Python the way it is in Java or C++; Python instead uses default arguments, *args/**kwargs, or the functools.singledispatch decorator to achieve similar flexibility.
Do I always need to call super() when overriding? No — only when you want to reuse or extend the parent’s behavior. If the subclass’s implementation is meant to fully replace the parent’s, omitting super() is intentional and correct.
What happens if I override a method with a different number of parameters? Python allows it, since there’s no compile-time signature checking, but it can break code that calls the method expecting the parent’s original signature — this is a design smell to watch for, since it usually violates the expectation that a subclass can be used anywhere the parent is expected (the Liskov Substitution Principle).
How do I call a specific ancestor’s method, not just the immediate parent’s? You can call it explicitly by class name, e.g. Flyable.move(self), bypassing the standard MRO-based super() lookup, as shown in the multiple inheritance example above.
Why did overriding eq break my set/dict usage? Defining __eq__ without also defining __hash__ sets __hash__ to None automatically in Python 3, making instances unhashable. Define both together if you need the object to be usable in sets or as dict keys.
Summary
Method overriding is how Python achieves polymorphism — the same method call producing different, type-appropriate behavior depending on an object’s actual class. The mechanism behind it, the Method Resolution Order, is a well-defined, inspectable sequence that Python walks on every attribute lookup, which is why super() reliably reaches the “next” implementation in that chain rather than some fixed, hardcoded parent. Understanding the MRO — especially once multiple inheritance enters the picture — turned overriding from something I used by trial and error into something I can reason about precisely, every time.
References
- Python Official Documentation: Inheritance and the class statement
- Python Official Documentation: super() built-in function
- Python Official Documentation: The Method Resolution Order (MRO)
